From 96769a7f4205b434bebea9fbd57d172f2ebeb1c4 Mon Sep 17 00:00:00 2001 From: Snowflake Date: Sun, 7 Jun 2026 19:35:17 +0800 Subject: [PATCH] Preparing for releasing. --- DEPLOY.md | 214 +++++++---- LICENSE | 71 ++++ LICENSE_EN | 73 ++++ OfficialWebsite/Projects.txt | 31 ++ OfficialWebsite/index.html | 674 +++++++++++++++++++++++++++++++++++ OfficialWebsite/swsec.png | Bin 0 -> 37174 bytes README.md | 73 ++++ main.rb | 28 +- method_executor.rb | 10 +- minesentinel.service | 27 ++ server_status_detector.rb | 10 +- uni_log_path.rb | 21 -- 12 files changed, 1140 insertions(+), 92 deletions(-) create mode 100644 LICENSE create mode 100644 LICENSE_EN create mode 100644 OfficialWebsite/Projects.txt create mode 100644 OfficialWebsite/index.html create mode 100644 OfficialWebsite/swsec.png create mode 100644 README.md create mode 100644 minesentinel.service delete mode 100644 uni_log_path.rb diff --git a/DEPLOY.md b/DEPLOY.md index ffce661..8fe6876 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,140 +1,220 @@ # MineSentinel 部署指南 -MineSentinel 是一个 Minecraft 服务器守护进程,每 10 秒检测服务器端口状态,发现崩溃时自动重启。 +``` +🛡️ MineSentinel +Minecraft 服务器守护进程 +``` + +MineSentinel 是一个 Minecraft 服务器守护进程,每 1.5 秒检测服务器端口状态,发现崩溃后自动重启,重启后进入冷却期(200s)防止循环重启。 ## 环境要求 -- **操作系统**: Linux (systemd) -- **Ruby**: >= 3.0(用到了 `Timeout.timeout` 和 `Process.spawn`) -- **权限**: root(需要执行 `systemctl` 管理 Minecraft 服务) -- **Minecraft 服务**: 已注册为 systemd 服务,服务名为 `minecraft` +| 项目 | 说明 | +|------|------| +| **操作系统** | Linux(需要 systemd),也支持 Windows(见下方适配指南) | +| **Ruby** | >= 3.0 | +| **权限** | root(systemd 场景) | +| **Minecraft 服务** | 已注册为 systemd 服务,默认服务名 `minecraft` | ## 文件结构 ``` -/opt/minesentinel/ # 推荐部署目录 -├── main.rb # 主程序入口 -├── method_executor.rb # 日志系统 + 重启逻辑 -├── server_status_detector.rb # TCP 端口检测 -└── uni_log_path.rb # 日志路径工具(可选) +/opt/minecraft/sentinel/ # 推荐部署目录 +├── main.rb # 主程序入口 —— 检测循环 + 冷却期 + 初始宽限期 +├── method_executor.rb # 日志系统 + 重启逻辑 + 重试计数器 +└── server_status_detector.rb # TCP 端口检测模块 + +minesentinel.service # systemd 单元文件(项目源码中) ``` +> `uni_log_path.rb` 已废弃。 + ## 部署步骤 ### 1. 创建目录并复制文件 ```bash -mkdir -p /opt/minesentinel -cp main.rb method_executor.rb server_status_detector.rb /opt/minesentinel/ +mkdir -p /opt/minecraft/sentinel +cp main.rb method_executor.rb server_status_detector.rb /opt/minecraft/sentinel/ ``` -如果要用 `uni_log_path.rb` 也一并复制。 - ### 2. 确认 Minecraft systemd 服务存在 ```bash systemctl status minecraft ``` -如果你的 Minecraft 服务名不是 `minecraft`,需要修改 `method_executor.rb` 中所有 `systemctl xxx minecraft` 为你的实际服务名。 +如果服务名不是 `minecraft`,修改 `method_executor.rb` 中所有 `systemctl xxx minecraft` 为实际服务名(共 3 处:restart / kill / start),同时修改 `minesentinel.service` 中的 `Before=minecraft.service` 和 `Wants=minecraft.service`。 ### 3. 手动测试 -先前台跑一次确认无报错: - ```bash -cd /opt/minesentinel +cd /opt/minecraft/sentinel ruby main.rb +# 调试模式:LOG_LEVEL=debug ruby main.rb ``` -观察日志输出,确认能正常检测端口、识别服务状态。按 `Ctrl+C` 退出。 +### 4. 注册 systemd 服务 -调试模式(输出更详细): +项目源码中已提供 `minesentinel.service`,直接复制: ```bash -LOG_LEVEL=debug ruby main.rb +cp minesentinel.service /etc/systemd/system/ ``` -### 4. 注册为 systemd 服务 - -创建 `/etc/systemd/system/minesentinel.service`: +单元文件内容(`minesentinel.service`): ```ini [Unit] -Description=MineSentinel - Minecraft Server Auto-Restart Daemon +Description=MineSentinel Minecraft Server Monitor After=network.target +Before=minecraft.service +Wants=minecraft.service [Service] Type=simple -WorkingDirectory=/opt/minesentinel -ExecStart=/usr/bin/ruby /opt/minesentinel/main.rb +WorkingDirectory=/opt/minecraft/sentinel +ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/main.rb Restart=always -RestartSec=5 +RestartSec=10 StandardOutput=journal StandardError=journal -Environment=LOG_LEVEL=info +#Environment=LOG_LEVEL=debug +#Environment=RUBY_DEBUG=1 +WatchdogSec=210 [Install] WantedBy=multi-user.target ``` -然后: +| 参数 | 说明 | +|------|------| +| `Before=minecraft.service` | MineSentinel 先于 MC 启动,消除守护盲区 | +| `Wants=minecraft.service` | 软依赖 MC 服务,MC 启动失败不影响 MineSentinel | +| `RestartSec=10` | MineSentinel 自身崩溃后 10s 重启 | +| `WatchdogSec=210` | 必须大于冷却期(200s),防止 watchdog 误杀 | + +启动顺序: + +``` +开机 → network.target → MineSentinel → (300s 宽限期) → minecraft 启动 → 端口就绪 +``` ```bash systemctl daemon-reload -systemctl enable minesentinel +systemctl enable minesentinel # 同时也会触发 minecraft(Wants=) systemctl start minesentinel ``` ### 5. 查看日志 ```bash -# 实时日志 -journalctl -u minesentinel -f - -# 最近 50 条 -journalctl -u minesentinel -n 50 - -# 只看错误 -journalctl -u minesentinel -p err +journalctl -u minesentinel -f # 实时 +journalctl -u minesentinel -u minecraft -f # 双服务合并 ``` -## 日志级别说明 - -通过 `Environment=LOG_LEVEL=xxx` 控制: - -| 级别 | 说明 | -|------|------| -| `debug` | 全部输出,含每次巡检结果、TCP 响应时间 | -| `info` | 启动/重启/恢复等关键操作 | -| `warn` | 服务器异常、超时、强制终止 | -| `error` | 重启失败、重试次数耗尽 | - -默认 `info`,生产环境推荐 `info`;排查问题时临时改为 `debug`。 - ## 工作流程 ``` -每 10 秒循环: +MineSentinel 启动: + ├─ 进入初始宽限期(300s),等待 MC 端口就绪 + └─ 宽限期结束 → 进入正常监控循环 + +每 1.5 秒循环: ├─ TCP 连接 127.0.0.1:25565 - ├─ 端口开放 → debug 日志,继续等待 - ├─ 端口关闭 → 记录 warn,触发重启 - │ └─ systemctl restart minecraft(最多重试 10 次) - │ ├─ 30 秒内完成 → 成功,计数器归零 - │ └─ 超时 → kill -9 → systemctl kill → systemctl start - └─ 10 次全部失败 → 进入空闲等待,直到手动恢复 + ├─ 端口开放 → 继续循环 + ├─ 端口超时 → 仅记录日志(不重启,可能是网络抖动) + └─ 端口关闭 → 触发重启 → 冷却 200 秒等待启动 + └─ 连续失败 10 次 → 进入空闲等待,直到手动恢复 ``` +## 可调参数 + +| 参数 | 文件 | 行 | 默认值 | +|------|------|-----|--------| +| 初始宽限期 | `main.rb` | 31 | `300` s | +| 检测间隔 | `main.rb` | 69 | `1.5` s | +| 冷却期 | `main.rb` | 58 | `200` s | +| systemctl 超时 | `method_executor.rb` | 17 | `30` s | +| 最大重试 | `method_executor.rb` | 23 | `10` 次 | +| 检测端口 | `server_status_detector.rb` | 60 | `25565` | +| TCP 超时 | `server_status_detector.rb` | 23 | `10` s | + +## 日志级别 + +`Environment=LOG_LEVEL=xxx`,默认 `debug`(未设置时),生产推荐 `info`。 + +| 级别 | 内容 | +|------|------| +| `debug` | 每次巡检、TCP 响应时间、systemctl PID | +| `info` | 启动/宽限期/重启/恢复/冷却期 | +| `warn` | 端口超时/关闭、强制终止 | +| `error` | 重启失败、重试耗尽 | + +## 跨场景适配指南 + +### 无 systemd(Docker / screen / 手动启动) + +核心:替换 `method_executor.rb` 第 86 行的重启命令,并移除第 80-83 行 root 权限检查。 + +**Docker:** +```ruby +pid = spawn "docker restart mc-server" +``` + +**screen:** +```ruby +system "screen -S mc -X quit" +sleep 2 +system "screen -dmS mc java -Xmx4G -jar server.jar nogui" +``` + +**supervisor:** +```ruby +pid = spawn "supervisorctl restart minecraft" +``` + +### Windows 环境 + +核心逻辑跨平台,仅需适配重启方式。`method_executor.rb` 修改: + +```ruby +# 删除第 80-83 行 root 权限检查(Windows 无 Process.euid) + +# 第 86 行替换为(直接调 Java 或 bat): +pid = spawn "cmd /c start /B java -Xmx4G -jar C:\\mc\\server.jar nogui" +``` + +日志输出到文件(Windows 无 journal)—— 在 `method_executor.rb` 第 26 行改为: + +```ruby +@@logger = Logger.new("C:\\mc\\sentinel.log", "daily") +``` + +以管理员身份运行终端,`ruby main.rb` 即可。 + +### 监控其他服务 + +`server_status_detector.rb` 的 `test_port` 是通用 TCP 检测,改端口和 IP 即可监控任意服务: + +```ruby +# server_status_detector.rb 第 60 行 +res = test_port('192.168.1.100', 3306) # MySQL +res = test_port('127.0.0.1', 80) # Nginx +``` + +配合修改 `method_executor.rb` 中的重启命令即可适配。 + ## 常见问题 -**Q: 报 `Errno::EPERM`?** -A: 需要 root 权限执行。确认 systemd service 以 root 运行,或 `sudo ruby main.rb`。 +**Q: 报 `Errno::EPERM`?** A: 需 root 权限或 `sudo ruby main.rb`。 -**Q: 如何修改检测端口?** -A: 编辑 `server_status_detector.rb` 第 52 行,修改 `test_port` 的端口参数。 +**Q: 端口一直超时但不重启?** A: 设计如此。`:timeout` 只记录不重启,仅 `:closed` 触发重启。 -**Q: 如何修改检测间隔?** -A: 编辑 `main.rb` 第 33 行,修改 `sleep 10` 的值。 +**Q: 启动时 MineSentinel 反复重启 minecraft?** A: 检查 `INITIAL_GRACE` 是否小于 MC 实际启动时间,适当调大。 -**Q: 如何修改重启超时时间?** -A: 编辑 `method_executor.rb` 第 9 行,修改 `PERSISTENCE_PERIOD`。 +**Q: 如何修改 Minecraft 服务名?** A: 编辑 `method_executor.rb`,搜索 `systemctl xxx minecraft`(3 处),替换 `minecraft` 为实际服务名。同时修改 `minesentinel.service` 中的 `Before=` 和 `Wants=`。 + +## 许可证 + +MineSentinel 源码采用 [木兰宽松许可证 v2](http://license.coscl.org.cn/MulanPubL-2.0)。品牌标识(Logo / 项目名称)保留所有权利,未经许可不得用于衍生项目。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..844bd24 --- /dev/null +++ b/LICENSE @@ -0,0 +1,71 @@ +木兰公共许可证, 第2版 + +木兰公共许可证, 第2版 + +2021年5月 http://license.coscl.org.cn/MulanPubL-2.0 + +您对“贡献”的复制、使用、修改及分发受木兰公共许可证,第2版(以下简称“本许可证”)的如下条款的约束: + +0. 定义 + +“贡献” 是指由“贡献者”许可在“本许可证”下的受版权法保护的作品,包括最初“贡献者”许可在“本许可证”下的作品及后续“贡献者”许可在“本许可证”下的“衍生作品”。 + +“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + +“法人实体” 是指提交贡献的机构及其“关联实体”。 + +“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的“控制”是指拥有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + +“衍生作品” 是指基于“贡献”创作的作品,具体包括对全部或部分“贡献”进行修改、重写、翻译、注释、组合或与之链接(包括动态链接或静态链接)而形成的作品。仅与“贡献”进行进程间通信或系统调用的作品是独立作品,不属于“衍生作品”。 + +“对应源代码” 是指生成、安装和(对于可执行作品)运行目标代码所需的所有源文件和与之关联的接口定义文件,以及控制这些活动的脚本,但不包括编译环境、编译工具、云服务平台(如果有)。 + +“分发” 是指通过任何媒介向他人提供“贡献”或“衍生作品”的行为,以及利用“贡献”或“衍生作品”通过网络远程给用户提供服务的行为,例如:通过利用“贡献”或“衍生作品”搭建的云服务平台提供在线服务的行为。 + +1. 授予版权许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、“分发”其“贡献”或“衍生作品”,不论修改与否。 + +2. 授予专利许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销的情形除外)专利许可,供您使用、制造、委托制造、销售、许诺销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”中的专利权利要求,而不包括仅因您对“贡献”的修改而将必然会侵犯到的专利权利要求。如果您或您的“关联实体”直接或间接地,就“贡献”对任何人发起专利侵权诉讼(包括在诉讼中提出反诉请求或交叉请求)或发起其他专利维权行动,则“贡献者”根据“本许可证”授予您的专利许可自您发起专利诉讼或专利维权行动之日终止。 + +3. 无商标许可 + +“贡献者”在“本许可证”下不提供对其商品名称、商标、服务标识或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用的情形除外。 + +4. 分发限制 + +您可以将您接收到的“贡献”或您的“衍生作品”以源程序形式或可执行形式重新“分发”,但必须满足下列条件: + +(1)您必须向接收者提供“本许可证”的副本,并保留“贡献”中的版权、商标、专利及免责声明;并且, + +(2)如果您“分发”您接收到的“贡献”,您必须使用“本许可证”提供该“贡献”的源代码副本;如果您 “分发”您的“衍生作品”,您必须: + +(i)随“衍生作品”提供使用“本许可证”“分发”的您的“衍生作品”的“对应源代码”。如果您通过下载链接提供前述“对应源代码”,则您应将下载链接地址置于“衍生作品”或其随附文档中的明显位置,有效期自该“衍生作品”“分发”之日起不少于三年,并确保接收者可以获得“对应源代码”;或者, + +(ii)随“衍生作品”向接收者提供一个书面要约,表明您愿意提供根据“本许可证”“分发”的您“衍生作品”的“对应源代码”。该书面要约应置于“衍生作品”中的明显位置,并确保接收者根据书面要约可获取“对应源代码”的时间从您接到该请求之日起不得超过三个月,且有效期自该“衍生作品”“分发”之日起不少于三年。 + +5. 违约与终止 + +如果您违反“本许可证”,任何“贡献者”有权书面通知您终止其根据“本许可证”授予您的许可。该“贡献者”授予您的许可自您接到其终止通知之日起终止。仅在如下两种情形下,即使您收到“贡献者”的通知也并不终止其授予您的许可: + +(1)您在接到该终止通知之前已停止所有违反行为; + +(2)您是首次收到该“贡献者”根据“本许可证”发出的书面终止通知,并且您在收到该通知后30天内已停止所有违反行为。 + +只要您下游的接收者遵守“本许可证”的相关规定,即使您在“本许可证”下被授予的许可终止,不影响下游的接收者根据“本许可证”享有的权利。 + +6. 例外 + +如果您将“贡献”与采用GNU AFFERO GENERAL PUBLIC LICENSE Version 3(以下简称“AGPLv3”)或其后续版本的作品结合形成新的“衍生作品”,且根据“AGPLv3”或其后续版本的要求您有义务将新形成的“衍生作品”以“AGPLv3”或其后续版本进行许可的,您可以根据“AGPLv3”或其后续版本进行许可,只要您在“分发”该“衍生作品”的同时向接收者提供“本许可证”的副本,并保留“贡献”中的版权、商标、专利及免责声明。但任何“贡献者”不会因您选择“AGPLv3”或其后续版本而授予该“衍生作品”的接收者更多权利。 + +7. 免责声明与责任限制 + +“贡献”在提供时不带有任何明示或默示的担保。在任何情况下,“贡献者”或版权人不对任何人因使用“贡献”而引发的任何直接或间接损失承担任何责任,不论该等损失因何种原因导致或者基于何种法律理论,即使其曾被告知有该等损失的可能性。 + +8. 语言 + +“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何不一致,以中文版为准。 + +条款结束 \ No newline at end of file diff --git a/LICENSE_EN b/LICENSE_EN new file mode 100644 index 0000000..1347d0e --- /dev/null +++ b/LICENSE_EN @@ -0,0 +1,73 @@ +Mulan Public License,Version 2 + +Mulan Public License,Version 2 (Mulan PubL v2) + +May 2021 http://license.coscl.org.cn/MulanPubL-2.0 + +Your reproduction, use, modification and Distribution of the Contribution shall be subject to Mulan Public License, Version 2 (this License) with following terms and conditions: + +0. Definition + +Contribution means the copyrightable work licensed by a particular Contributor under this License, including the work licensed by the initial Contributor under this License and its Derivative Work licensed by any subsequent Contributor under this License. + +Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. + +Legal Entity means the entity making a Contribution and all its Affiliates. + +Affiliates mmeans entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. + +Derivative Work means works created based on Contribution, specifically including works formed by modifying, rewriting, translating, annotating, combining or linking to all or part of Contribution (including dynamic linking or static linking). Works which only communicate with Contribution through inter-process communication or system call, are independent works, rather than Derivative Work. + +Corresponding Source Code means all the source code needed to generate, install, and (for an executable work) run the object code including the interface definition files associated with source files for the work, and scripts to control those activities, excluding of compilation environment and compilation tools, cloud services platform (if any). + +Distribute (or Distribution) means the act of making the Contribution or Derivative Work available to others through any medium, and using the Contribution or Derivative Work to provide online services to users, such as the act of providing online services through a cloud service platform built using Contributions or Derivative Works. + +1. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or Distribute its Contribution or Derivative Work, with modification or not. + +2. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to use, make, have made, sell, offer for sale, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, excluding of any patent claims solely be infringed by your modification. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that any Contribution infringes patents, then any patent license granted to you under this License for the Contribution shall terminate as of the date such litigation or activity is filed or taken. + +3. No Trademark License + +No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. + +4. Distribution Restriction + +You may Distribute the Contribution you received or your Derivative Work, whether in source or executable forms, provided that you meet the following conditions: + +1) You must provide recipients with a copy of this License and retain copyright, trademark, patent and disclaimer statements in the Contribution; and, + +2) If you Distribute the Contribution you received, you must provide copies of the Contribution’s source code under this License; + +If you Distribute your Derivative Work, you have to: + +(i) accompanying the Derivative work, provide recipients with Corresponding Source Code of your Derivative Work under this License. If you provide the Corresponding Source Code through a download link, you should place such link address prominently in the Derivative Work or its accompanying documents, and be valid no less than three years from your Distribution of the particular Derivative Work, and ensure that the recipients can acquire the Corresponding Source Code through the link; or, + +(ii) accompanying the Derivative Work, provide recipients with a written offer indicating your willingness to provide the Corresponding Source Code of the Derivative Work licensed under this License. Such written offer shall be placed prominently in the Derivative Work or its accompanying documents. Without reasonable excuse, the recipient shall be able to acquire the Corresponding Source code of the Derivative work for no more than three months from your receipt of a valid request, and be valid no less than three years from your Distribution of the particular Derivative Work. + +5. Breach and Termination + +If you breach this License, any Contributor has the right to notify you in writing to terminate its license granted to you under this License. The license granted to you by such Contributor terminates upon your receipt of such notice of termination. Notwithstanding the foregoing, your license will not be terminated even if you receive a notice of termination from Contributor, provided that: + +1) you have cured all the breaches prior to receiving such notice of termination; or, + +2) it’s your first time to receive a notice of termination from such Contributor pursuant to this License, and you have cured all the breaches within 30 days of receipt of such notice. + +Termination of your license under this License shall not affect the downstream recipient's rights under this License, provided that the downstream recipient complies with this License. + +6. Exceptions + +If you combine Contribution or your Derivative Work with a work licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (hereinafter referred to as “AGPLv3”) or its subsequent versions, and according to the AGPLv3 or its subsequent versions, you have an obligation to make the combined work to be licensed under the corresponding license, you can license such combined work under the license, provided that when you Distribute the combined work, you also provide a copy of this License to the recipients, and retain copyright, trademarks, patents, and disclaimer statements in the Contribution. No Contributor will grant additional rights to the recipients of the combined work for your license under AGPLv3 or its subsequent versions. + +7. Disclaimer of Warranty and Limitation of liability + +CONTRIBUTION ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE CONTRIBUTION, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +8. Language + +THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. + +END OF THE TERMS AND CONDITIONS diff --git a/OfficialWebsite/Projects.txt b/OfficialWebsite/Projects.txt new file mode 100644 index 0000000..b2f675e --- /dev/null +++ b/OfficialWebsite/Projects.txt @@ -0,0 +1,31 @@ +[[projects]] +name = "MineSentinel" +description = "Minecraft 服务器智能守护进程,TCP 端口检测 + 崩溃自动重启 + 防循环冷却期" +repo_url = "https://gitee.com/snoware/minesentinel" +demo_url = "https://swe-iss.rth1.xyz/softwares/minesentinel" +doc_url = "https://gitee.com/snoware/minesentinel/blob/master/DEPLOY.md" +tags = ["Ruby", "Minecraft", "Server", "Monitoring", "Daemon", "SNOWARE"] + +[[projects]] +name = "btcli" +description = "一款在线命令行翻译工具,支持中文及其他语言,配备TUI界面" +repo_url = "https://gitee.com/ironbayberry/btcli" +demo_url = "https://swe-iss.rth1.xyz/softwares/btcli" +doc_url = "https://docs.rs/btcli" +tags = ["Rust", "CLI", "Translation", "TUI", "SASWE"] + +[[projects]] +name = "Reverberation" +description = "Story teller, it's more lightweight than nano and more convenient to use than echo." +repo_url = "https://gitee.com/ironbayberry/reverberation" +demo_url = "https://swe-iss.rth1.xyz/softwares/reverberation" +doc_url = "https://docs.rs/reverberation" +tags = ["CLI", "Chinese", "open-source", "SASWE", "Rust"] + +[[projects]] +name = "Mint Runtime" +description = "Security-featured runtime for Lua with i18n support, bytecode verification, and fine-grained access control." +repo_url = "https://gitee.com/snoware/mint" +demo_url = "https://swe-iss.rth1.xyz/softwares/mint" +doc_url = "https://docs.rs/mintrt" +tags = ["CLI", "Chinese", "open-source", "SASWE", "Rust", "Lua", "Runtime", "Security", "i18n"] diff --git a/OfficialWebsite/index.html b/OfficialWebsite/index.html new file mode 100644 index 0000000..dfe7db7 --- /dev/null +++ b/OfficialWebsite/index.html @@ -0,0 +1,674 @@ + + + + + + + MineSentinel — Minecraft 服务器守护进程 + + + + + +
+
+
+
+
+ +
+
+
+ SW Security + + + + + + + +
+

🛡️ MineSentinel

+

Minecraft 服务器智能守护进程

+

TCP 检测 · 自动重启 · 防循环冷却

+

SCU 团队 拥有 · 编写者 S.A. [@SNOWARE]

+
+ + +
+
+

🔍 高频检测

+

每 1.5 秒对 127.0.0.1:25565 发起 TCP 连接,毫秒级感知服务器状态变化。

+
+
+

🧠 智能判断

+

严格区分端口超时与端口关闭——超时仅记录日志(避免网络抖动误判),关闭才触发重启。

+
+
+

⏳ 冷却期保护

+

重启后等待 200 秒,初次启动有 300 秒宽限期,彻底防止 MC 启动缓慢导致循环重启。Watchdog 同步适配(210s)。

+
+
+

🛡️ 重试上限

+

连续 10 次重启失败后进入空闲等待模式,每 10 秒检测端口,恢复后自动退出。

+
+
+

🌍 跨平台

+

核心逻辑纯 Ruby 实现,支持 systemd / Docker / screen / Windows,轻松适配任意进程管理器。

+
+
+

📝 日志完备

+

四级日志(debug / info / warn / error),输出到 systemd journal,排查问题一目了然。

+
+
+ + +
+

📋 日志示例

+

通过 journalctl -u minesentinel -f 实时查看,以下为一次完整崩溃恢复过程的日志输出:

+
+
18:16:17DEBUG self_diag: TCP check OK — 0.0002s
+
18:16:17DEBUG mainloop: Server health check passed.
+
18:16:18DEBUG self_diag: TCP check OK — 0.0003s
+
18:16:18DEBUG mainloop: Server health check passed.
+
18:16:20WARN self_diag: TCP port closed — server is down.
+
18:16:21WARN mainloop: Server health check FAILED: Server died for unknown reason, and will be restarted.
+
18:16:21WARN mainloop: Action: Record → Server died!
+
18:16:21INFO mainloop: Action: ForceResetServerProcess triggered.
+
18:16:21INFO reset: Attempt #1 to reset Minecraft server.
+
18:16:21INFO reset: Spawning 'systemctl restart minecraft' (PID will be captured)...
+
18:16:21DEBUG reset: systemctl restart spawned with new PID.
+
18:16:51WARN reset: 'systemctl restart' timed out after 30s. Force-killing process...
+
18:16:51WARN reset: Running 'systemctl kill minecraft' (last resort)...
+
18:16:52INFO reset: Running 'systemctl start minecraft' (rebirth)...
+
18:16:52INFO reset: Force-reset sequence completed.
+
18:16:52INFO mainloop: Cooling down 200s for server startup...
+
... ... ...
+
18:20:12INFO mainloop: Cooldown finished, resuming monitoring.
+
18:20:14DEBUG self_diag: TCP check OK — 0.0002s
+
18:20:15DEBUG mainloop: Server health check passed.
+
+

💡 日志中 PID、精确时间戳等敏感信息已脱敏。实际输出格式与 journald 一致,支持 --since-f 等标准过滤参数。

+
+ + +
+

📦 部署教程

+ +

1. 环境要求

+ + + + + + +
项目说明
操作系统Linux(需 systemd),也支持 Windows
Ruby>= 3.0
权限root(systemctl 需要)
MC 服务已注册为 systemd 服务,默认名 minecraft
+ +

2. 获取源码

+

方式一:Git 克隆(推荐,仅拉取必要文件,排除仓库冗余)

+
git clone --depth 1 --single-branch \
+  https://gitee.com/SCU_team/mine-sentinel.git \
+  /opt/minecraft/sentinel
+# --depth 1      仅拉取最新提交,不下载历史记录
+# --single-branch 仅拉取 master 分支
+

方式二:手动复制

+
mkdir -p /opt/minecraft/sentinel
+cp main.rb method_executor.rb server_status_detector.rb \
+   /opt/minecraft/sentinel/
+ +

3. 手动测试

+
cd /opt/minecraft/sentinel
+ruby main.rb
+# 调试模式: LOG_LEVEL=debug ruby main.rb
+ +

4. 注册 systemd 服务

+

项目源码中已提供 minesentinel.service,直接复制:

+
cp minesentinel.service /etc/systemd/system/
+

单元文件内容:

+
[Unit]
+Description=MineSentinel Minecraft Server Monitor
+After=network.target
+Before=minecraft.service
+Wants=minecraft.service
+
+[Service]
+Type=simple
+WorkingDirectory=/opt/minecraft/sentinel
+ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/main.rb
+Restart=always
+RestartSec=10
+StandardOutput=journal
+StandardError=journal
+#Environment=LOG_LEVEL=debug
+WatchdogSec=210
+
+[Install]
+WantedBy=multi-user.target
+ +

🔗 启动顺序

+

MineSentinel 通过 Before=minecraft.service 确保先于 MC 启动,消除守护盲区。首次启动有 300 秒宽限期等待端口就绪:

+
开机 → network.target + │ + ├─ MineSentinel 启动(最先) + │ └─ 进入 300s 宽限期,等待 MC 端口就绪 + │ + └─ minecraft 随后启动(Before=) + └─ 端口打开 → MineSentinel 恢复正常监控
+ +
systemctl daemon-reload
+systemctl enable minesentinel    # 同时触发 minecraft(Wants=)
+systemctl start minesentinel
+ +

5. 查看日志

+
journalctl -u minesentinel -f          # 实时
+journalctl -u minesentinel -u minecraft -f  # 双服务合并
+ +

⚙️ 可调参数

+ + + + + + + + +
参数文件默认
初始宽限期main.rb31300 s
检测间隔main.rb691.5 s
冷却期main.rb58200 s
systemctl 超时method_executor.rb1730 s
最大重试method_executor.rb2310 次
检测端口server_status_detector.rb6025565
+ +

🔄 工作流程

+
MineSentinel 启动: + └─ 300s 宽限期 → 等待 MC 端口就绪(避免误判) + +每 1.5 秒循环: + ├─ TCP 连接 127.0.0.1:25565 + ├─ 端口开放 → 继续循环 + ├─ 端口超时 → 仅记录日志(不重启) + └─ 端口关闭 → 触发重启 → 冷却 200 秒 + └─ 连续 10 次失败 → 空闲等待
+ +

🪟 Windows / Docker / 其他场景

+
    +
  • Docker:替换重启命令为 spawn "docker restart mc-server"
  • +
  • screen:system "screen -S mc -X quit"; system "screen -dmS mc java ..."
  • +
  • Windows:删除 root 检查,替换为 spawn "cmd /c start /B java ...",日志输出到文件
  • +
  • 监控其他服务:修改 test_port 的 IP 和端口即可
  • +
+

详细适配指南见 DEPLOY.md

+
+ + +
+
+
+ + +
+ + ⭐ Gitee 仓库 + 📖 完整文档 +
+ + + + + +
+ + + + diff --git a/OfficialWebsite/swsec.png b/OfficialWebsite/swsec.png new file mode 100644 index 0000000000000000000000000000000000000000..de04dbf0e4bad67658e7fa05dc76edd4836cadbe GIT binary patch literal 37174 zcmdRV1y>wh&}Kq#cXx;2?(PJa-~_jzgKO~M?(Ts=a36GVmjHp_?t{DRBzgDS-Jh`M zFo*7*?!Na{)qSe!shS8?WmyzNLc}+3-k`|KNvXei^Hvq|k%EVXTyd_p)P($d3s9Gp zc=Ka|_yBSOZ6&TG{^m_>9MY2s4CEZaNlq8=<_$^MpU+#vLaac@1K~EBI<7iO9|X)C z?O9CB9ZfA*JnfwzcfWZfBc1CASpdvjY@A$e939C2ywJqd(alwu(#_4rT)@)AlH1hWoSWIygwvdvo!#7o*~FZa zo!Oj|)0~%^osYwk-IVgb;azPk|Ihdifd9k*5eVy_Z&=w`*#1oU&#{1ti;V?j7Jr5m zVHf)Q{r_`Ki1p7z{;!FN{JR2KE9B^ZB=bj1|07)s2Z%^LAhKghPDgt4CXP>DN?g;^ z==j+qo7u~7FHTgnph9_nnRgpHYL}^TQ(svlR!Ld?BD6%zAXcmB$g;xE(AzygC3pDi@kOtUh{8YzZmf z@@vg;@6CJ7JzmE4`hX*(C=-VKWR$zm{~YNYCW4%)Q=M(eD*8v;J0JuRGX{4`IX(Nd3ej_Etb~YDPmZ>bnA?Rs5HH^R-o7 zmX=R=ppAHkX2EAGs#`J0+ReiO+rQ#j1H5o3^w(*OzRY8mcN0Y2lI{_cV@QyPbo;1W z&duu;Ho_7Qr0Uo?O;A`wjH8`z(*?juAclb`a+IweCS^~Ii3-2SPzoj&e#1vm zdUb5};&-*|I zj`Z(BHq{Z8ksPdXJV0Z5E-2w;Lk@F)+DH3v7Ru9gdo-AjyDVPg6dvpX45_ZLec+<> zko;?8ri_!HbYMmSs_gjuVP;45xV>sMjN!qN)7|X&nGYC7NZQ}crr=M>bsCCH&JuP* zX(koT;aAO~GelyBR?uO%Fc%Aa&lLxCzyuG$E}z*^!`#qq@O^WO^oZe-UM3%=$uui( z5xs*GUut4|<0L}-`COyJQtMo?rEx|w;S=tLI{pddKHOtXE+BrEEM)%?B9OVbm@jGs zPdSpKPO8d4W7t{}Y~%;6J3r0DxXg8p8YvJMCdNTq3&$`2Hjq9)VFd4~PK=3s>m+F_PYpU|NqN5Y)d2ub>2IcZg@KNH_PzTLlZPdzq z!ti7b(m{-nr3@hyC58fj>l)d>XiD?M%a!!NP+J)J9$|lUOJ|;6ZXUTT_}3_~OE7^* z)1GiP9|{{|DZRkNy;%9&v*!sG$|c=fH%HH3;9fyv1%TpCTN*?8pG zq@m8FTS7lxypbrx=HnuEfOx2q=_Nnudx}E|Tdn@i2A`crD0pshJ=5ax?!AG2w#as* zL(MtB35E;${ZAyP-k4xHH8C*J-<6Ft#MS90Iu^4jb$J}FPia>x!KRcN$Ni^(3lV=-(G#2i4HC`bFIu+bnY?cnxOxZi0a?wo7m`| z$1S`qknx{+$i|(jf^i3-wWWN`Rro@s|4#*<;TNUI_l+Jz%lZLD^ZTcZ3YAo4w11v9 z%NSzK^nTW)^k!7H&{am-ZIC8x^=W!ofcP-MH&xuLKFR9%M;P?JCtaUq9JvaAea*e~ zH=q)|U)q(`89;Kb<0k6A?8Ja5nqTRx>}ON^BcY=vNvPff1R3`IwoXOvh+Ilpz0*5&ozv)O2kC;GK1{RAP65zCIpy!Kwoh`SA*%CDr5 zWxV`dMwX@Z@`_$zR;bHEEl!!Ya2@v_eas?*{YapfY@yG0{qttw)xf`>jO-k#vi?V3 zkpKrFi+XK` z9R*{>GvK{i%(BnlF_One!Q@|1{0qTWo)kL8S!v)|@sbE~b>?gd_>nrP@)$|!Q{jIC zF(;f|iP#+eiE!fr3PK5L)eQk1l?ljful~i`o)DwlEPw!SNd0-RgKrESjEkN*Z|a!| zk(x*G?P^@Tj0*EeLe*boe^PG3_k$<4zn;UP00WlvNYcJIWR_sZPQ!ouZX65cz4mL< z_|L|N&gRUj95_p_AuMAW39}ik_p*PV4cN+jJlK*t&G-kf7!6gqWyVFg!|{A;K=(Rn z#t<>|%*f9o+Srqi|6B=XhLmz(0Sz?p6OY}US58sTGetDWhU8tf_Sc~TT4WSQWXBH2KoJnVJ({c)XH zA*egB86FKYP#ckk8Z+=J|Ik5cUa!)F99lvFhz?(ZA1$*lfX4M8{tr8HMsR5|(JP0= zm`lwv9WJly0(u|JKR3>!q#sENg3mQrf`&TwuwNw-+_X-wFX=BlJ4{L1=-7< zi)Q0$(OhjGhdTb@1^*)DGWs~Jr>ja39+Xs8u*$fzbdDT2n`kdk=JYRA<&5FZ@4CX3 z`e06n9h@{_t{<#Xd8qvLuP$}z-1AmBpaTg;4bZ^N^}>>)PL?1>6>rWc=F@ z!*!Lo4XV;V*_g9$`*vZe$0s+u)Sc*HcwE5$AfloIdH7lHe_@DWBm08f=NpyC(xVw{qH6mW zTa^hB+dgghQ1uE20N(0vu5Tp zDr{%{|96ySQ-)w#Xg=fO^vX@Jiy$gO9Zh@vI{=BS@!MwBYH}Awy<6Zv?AetN!f$XC> z%vEFE#a}rRq(tp=SjNDV@_JW~{1b?bV1gK#t!K*r>~6DMwO2CdNyNVccha)g5?wOx zw&2t+aR4f(G6JvgfR2%`wrPTmw?PnTTY4zHZ8P#c4+)iv?Z0hWLV}*@_hy~0YSI$+ zDXe4>h}Y+0+T~pLlq`Kjvob+E!3gUIBI~-bhku)QKW4?_h{sR!7rGRhvnWGlH@m(1 zq?iR8-4?%EQ^ikYB{gLPniR@z38H_?|;aEIA*P zdQha;B*Bl2D?ZB5m)Z0IE`(XhS(zsoF;}F&6O0hQyntW*XFLD*V^8ZEq#xd6d0N%X z7dD1PxG35&E66vX(qw$NlO+P)x|Nq!3bL`IN@a``%py}@&3n_RoBs^ocsQzFbIl%_ z|EGn~QIg$V4OdF^UbCux{_qt_ou6#>7OPm&M(krK?H)NG`fxQ2qt~-=QK1eSbEJP@ zejkcDI!Yj4sEYa2_kqgpgzpO%ft=|GVvcWz$;ET!Y-wHWZ%5JIsQBp#spLxB_g^FU{>$ z%)db)J&jFCe#ko>-wcV6fuUI_MxGQeW!4>-&L19V)rw-9m4lQ{Mm04xAGEw@`T!sl z-Vet_{5XG=5~*I(@>3cfiE>Oj6MJMBI8jwOcCe6jn;DKpL9nKakBT_2?W37u!57lp z^?ZUYeS#j7o%nZ8ST0;33nXkMkS3*CNum$OEZrqlO^$a?4c|tyH)?&COLX7I$P7Dq zy;6B~1sa;${gq9+tcb6N95W_vT)!?Fi?4ksdW@oLk{A?LP9{%4Kb@PF_f$kLK`=UE zf%^Oc(+{J=*E!?ox$Peb%>R5Ai`Nh_0Kia%9rA|eH+HKmd}#G!03sS@s8lH7?QS6Q zv((WV5n078QEM?G8sCMG71u>W``$&x$1XT3sp!77e;P{X(()Ai#CP~MmP;ukCj+Vc zgWK)h0nOtb_rjf&-Olnlgk}kW5@Me{p#Z|CTuB_VX_%ub9K-1UcejD^x zZhJt0)xIm27C+@Qc}Ph`#ra)~46*czL9v(pl0Z(A=G!`^qGl0tB|USS@C~*>esQ%h z_35D%ESRe-DJ_9b&x|2=Ho>gKipaXL4CDRvT6~SdM$|%`xQFyHtQ<={zH*pmdWF<9 zkEA`C=NEKfuV6pX>Y2y3*M!zbL+%cJbI2@yM>oc@`W=uPw>_{p01xmxjK1yjd7iCb zl_m^z^zz5eAm;FmxJYU3xVEpvzY#UjRo)9y*3Z{ZphmE>v$L+Uu^_ns_7V12TMxzT z`GI}bu2>Qf3comTe*aE&+Gf}NX-fM=ns*}nuh zAg;iEcPJs~T7=lRN=KhJTZce>yNB{FX6-KKV4Nz!r+CFC!7`RBkQ#^2YXA1pRUjuR zB$45${{C4Lo&sILq>^rQ$&+s1)l#BpM~`InBDMJg7`HqEsx3z6=)N~?{#!*zp(=KX zWwK;|s!8^`lA||P-pf^4nMah=X=;Y@NN?r9FA528SRZ|3tbbb{ZBB1-X5M{jBJ1;Apya0-yG=YM!N z==5g5GnSaY_i^pjU*x)6r9X<(r5YW||LP!(O|(laH_iM6S4#_u;zDG+oM}-0NCQ5w zF}1aoKUo1k1~EI>aI~FZH|%w<-!X_GP0Sp^mo!JP8rMr`d7%?h=;N1tQ_wMduLIUX ze&G3PP5Pw%?4Jj|iv@qOv9WnjO&Cf&)^*shm(~hHETEQG#+)49c`TQ5#jN56ZJ?VW zuQ=eDArHpok;|*MKShIxg$v5hIpNJrE{9dYT};qGzU*->f7R?q5J z4*~A7y6%ZOpLSSP$Y?S7Y4EbU`i?&2=SKg>4>yQw}!0&o) zHat>FIfg8u6w{6bDn^VF_-5~Dcf{lhuF(^b6~ib9Yr|_qqN)P1ihCg4X)6uth(w~1 z{(X%bVCRqHU7?j}En$>o2Hu*4H8QnptmGYKSk90L{qtytkh}Jvj@aFCMp}I;06_;^rHYA4oEtRsDKC}4&AImNp zgfFY53;UeCMK{my@;QY{_M?t55gT#XGb)Jym)|!zb48*21}LG`aG9Ykng^W&YV@bf zq@H4%%!SfhJc*Es``zpEXUadDS}EBA$5%nQK+Z`EeY%1JIh?=a8so_Bop$W4E;wzw z$SLn{WbpT$z)N@pb(jL8+ZWeas;GCffyD>6ysKEAgQYxFTlOo%{&PdH5JN-&Bd7BxCkXPMT2c6mv z$7X{x`D#MoSjcK2#uLIdfAo@jkA+87T+XWS zCeQeb5o-zHmDJ#qQ8{P;u#KqXajK*}+yqTLf0m@%7Ydau(=4 zJjD8Wvz-%F{-~_kPi|!TWoFC{>N(UBAvoCGT~DK3(YFzwoi6AITaCxYOPE;XgXsFKn66(%oFQB~ z!DayM6@~OI#U#@nPM&B263AWM)fL`;itf>L$ab-c?(q!&Ntk0!Er4`N2WA*m^&tBT z+7j@A+PDmhDZ$9u5zoZkYmE*x@{pw(BfC*9sV$g^p!&oS#E_7>QzAlsNKXDc~d5j`|#x#e0~nhdD_=7961Zr&_19TM{{IHb;kC&0&T+7 z(o#JH?FYpGCPR;7iY8h5Q%8A3S(8N8r-W*{8Gg9=FXi2Of!tF^KK@6|{&t&R6T<#1 z>H_ES#!XBI$WAREq%N<6Ij81z7}d%ZI|3iAW%f3P8N{0;;Fv`njO4vf;=jj&L@9~X8&627 zstO^C90MAuu<<&qhX(EIaEN2(J8vnhHKFPXJv`j8<=Xp$4EFJ_v zs)P>Y_mWBpOEkZRP##^=jEBaF-kUn3s6mRScn4DiEAB&EfQcjY&erh>J!AVCQ( zB|3`%XOaXzL+QBL(nWTISouehB#EQYKE4{6pSskTc4E!zD`5Xy&vrR-f4k-c2Ralp z3?P9lGpL^dq;3S=m6YtFA%MU>;)OXc5FHBqBJOYyhpZ;>PD0z0vG~GmXDUp^b=F-U zxNGyH{*$uW)4^B{hXgM+%x{ClHE(&q-sFVD?{US~21>Xy})W zs3b+$Rcd-d`?n3fDCj>DN3vwi5MuaI%7svds}aHHBOVrHB#)7grJs+3PJHTNV(EiY3~V z`F=>9jir-h`aRhcnoipV_TAmjGLim~k&&z2+lV@jW&cMtR1&>1CQ0|c1S;85-ruzi zdI;-Ng+?2m191m-HcL>ATdzW?=7aS$5P12CRUdRzH98-j$**j8b{D<5MwU4zo@fJzLQs3u5ODB5f?4%Z z&gNt9xbN6)t6m$>`nVFJ$?SfTLn9y5{WReCG;mcah+D;N0~L|Fp_fPsrkmNObrB+x zh-yROsho<*YK4E7=meFGr^CvoJu|Xa!*qlNuY*nPy^2G|Usp5x=f~Ke}~+eMV#gzFb%5t=^I98k3z@=e6Y?RU4c6 zJL6p*RdPt%sr1jFmz|iaUVJUfW-V<%6fKJD%{fm(?*ePQi6SWAer_!Y?J#| zUY9SH*bsCdve5Nnn9jlF9RUr|{=2-_Uc~#MP#P7SOvX{E1?^HakDJWhcX^#tR9P5M z#fY0W9cIaC5Py_%*3{f#?|s?6gO%43(RFwEzI6u|6wKNd0+=)`UQsh=yhDGz40xG* z+E#hlzH$&u<+>i5pl?7dmnS-SY1i%9x$84^7_FHU*34RIAY8behxYJ%cW7{AyAu zL)WcO*I?G_Rq1R6<_Gv5&Rk~-1)}5&O)d7$&df;Oz22Y7B|l{>4E$oag{`X^;lUV6 zT*gr9&;&n-7=6m{V;O}Iqz}qJI8jJJl}&2ZOEMsiPD}F7JKP9z_m(G13>Uuyjl?(Z z1z+aYsOIqL6;mi-+mtelVww>L`%l!SkhVDxoQ3fMO}z2mjLclbj&@Cubsj7FUge>( zbJoWxvUJ2pk=ErIwo~gOHNcB^<~=Xw?R34~zKZyLoedfkJ|%85lv7vL3lnKb+WJaY zcx?8xJ2OeUg4*q9(keiW8we22`qJnW=Zn>eY zuBGiSKM&`jDH!(|w>SLyDM6Wsy!C!JYjBYNcLdY}3g+nr(SiJYma}$KO_sR;D_p%WR>7f$ zBLAoHrJfL--fBdICDk}Mi;R>4biG78h}5zbD>yL7_jiJue)aGkaxfEFN~rh4Na~28 z$%OHm6Y83lLQ8ie@2j&#u5^_WWfU#7bsT$11qnE_ET~t&dQUGXGFAC^Moi;4iNvN&?O-L>w3G-7SY;}o$=$+fvAUG zUBgeA@8}U}B79$I-=!ug-k;l1LWlDh*+8ihmAjdm{zycJq9dMvJbR=4@#COR-ig%P zXIKoV5cuH>1^H~w0%Nn7WXxhQ#28h9hPwJxAy!DLjSP#k3ff6C?mTF*5|pRRIIwKc zzs%-SbMh_eTh#SFA^@4B37)$6?p_`l_!tgvfM{$=hdD+sTza=UpD8E0KBd0f^AzBm zlE+zl>{uJkT6@~%tVpz6d%2JBz8lWEwme@(pB{H8lsD1B>gC5(p^+SU{-t6o>F@Aa zfsGb%pZBYJB6WhN04e=vL0Ymc!3y;%JMOY|Je z_t=bVOZ2(ymFw9`*$x+P>6M3zsViP!J*Yw>xIwC`RI(7(I+zxq zQKDJ-L9PUMyqI;iT_zM}^>nt?dR51tiYJaFo@xPvWZiogqpYG-NnL_D(9BHgEv3Xo z{$;4z4kn?oRl~LEu%mO_G&%Itqzn=^R)rD;f$D@b8q|Gn0a)-|_f6QXaro;OU5|(h z{^Qd}bd{ATH4-znjbaKw1dIFoimsdGpYy^+H-%XI0CPR3$CGBj zs@I;23OcTnRT#(UXtVxg8^5|3O+GA&YMFq5kb%`iNQVMJglzyQc$LG#(C zRJjifpE-edXg}duI^ii&8wq$N`&jQH1kz?G0#1$@NPIUtFP!`#%nW_xJ7~tbkr;LB zJltPDHWu`ws>JW{<{W~9fn@gyWEmpkoRP0DGjZoT)%X``VQviCazJ$Jxn3mILS1jT z^W-n_A}z_qvI@KtI4Cwsl&&_Iq%-gVtS(bZ0cQpv{Fcz{!m(H{Ws(u0qQD+bS#sn)Qht(0f4AVTep7o;QWv^{) zM&apZ1~+$JD?M$pzHDCQ@!PXW!*iu$dLNxFP8tkGXn{7&sA7o^>P6T*OXTXHX{eQj{oTH=Em;6zb zg+1VN)$2&s%as|iNHM_LMkckF(`QlWtl9(}jI3nfD5g6__r@~j+iF-trj zX7eWSI?rHoZyL&Sa5fQY9M_Q5d3Jh+Ey}V2xSlBQewD>?C+Nbe!1;> zecrNr`V0X_o~aQoOqD_@Yj$npd?nRR1X_6p=b@SgyhJsl=_4&<+1!opx>eq21T0wq?npECK5f;R)XCLDs$DdMLEFNwP3Rc9%Du}1e4;CTw1(h&(8SJAczp>KL+A+ ziXCioGV?xJPEg`Pe=w9_hr+D|GAr;VpaGH)m}?T%$fcBC12Ltu%w{Ygf)VSEuM7j33*y+E+xrZ zT(B^ty^_g3+!LDuGoT3z#x;xYc3C%(nyXZ)SmH=^dK%tV(&_M>Tr`&E0OuI9*8zi~ zY=U5Pu!{dh5_bQ}oh6H&aVsx#`mP|SSo(b^JB^l>Nfk-dw|PQHZL_Rktq=|jJ%NNC zS?J1F#!gy{=<)P=-Sx5n!BaEOZ7Q64v?!uGCnFL8k@Ea-Wk23Mv?wB;1AWrAaRk+z9j(<5`h5{ z4-yC&gri&;hIILk?5_JN)(ZRa7QAPp;zKwp-D5_!Vo}N&jElKJ6M;O(3KE~#+~k;0 zgfyR0uYkJz509RDzP|*5WSfnGa_zb56Vi239h*c{P$}(=IvvfUKU)@c8addGwj|KH z{ncZBrYDHIs;BiZtcmIrUB@tp1%`ydRxUL zt`Ot29xcYF)VyNh=9WV}Z0&|iAITHsx9t9XUFBtO5W>_)U{ShJ9=)*?{%WTnHmFOk z5sa*|^X|HnG)xCvoh(L&SP~qSPl0F+kp+lXq|EzaW1GZ#1}fc%gQ0Q!nCHpWNgxFT-KL?# zdiztOSzFk%3JV%7$g$@=!-^vP*IZ1w_xcXMV%fo15M*Cjfu!3yjn2bayAsqeDl^^7 zq*HBo_XOnB>VUs`(%oc0IJ=hmCZ(D?YD{eSW6pDUahIb0uP#Q9ZD{Mui?{u$E>yN6 z%!w|Pg`}iCZ9qXDfuAaO(#`2CM9fnV-fpZf9lBY%85}WPyA@?<6YZQn!bz-oweUVH zIMNsTqmdVl@$16^?{g?;%*NE&g=iZBD~D$0dWo!E`T=!=eo%iuYWdBW`){VPNmE1d zRQS}?RQmU_-#en~Id$5*Q5LGt2Lm3A6hOkLIf6y_cA1q_$3)*rz&y~77rTBOJy1Ev zpV#0|2evdHb2}gi#6xJT@wJUt@lFX!zy{{SC&oBHqVIOl*Bb@zqn-z$ocD>R5!2r` z@4FtQQAs?^IqeZ=W$6{?VRN*p+lq6y)JDQ?gU`jk@~6vDcC}$62x^Y!wR8 zDg-g=oi`~(PqU&cy}iAi^#d?71|O4BZ@0%ESL>X!V-QQjoPa1~c9%=2jIn}39v2mRs1IXO)3e+2WBJ^VUH z=ihj-(O&2f?yyZtM(%f6`!~w@^f}rKk3SC{wiKTKY zC0t0AH*LQAd7WcNubaZN0XNZ7h*Nrn$=ypeKeZX9R!Yz}FxuEc7Uhi!Rj(&ArHbjH zf9+;!;yXG)5tA_=~K4 zla*@9ysCOIa7C1dBJ#@8u5LeR#fEavphHx@kH%DkL2uhv@0G0}mDR2BOU0zBq=FHW zTtQ;K;GR!F@WBUNy_c7qX6NlQ0Z7GDf2TO(lh*<l}z%f=&&&6XI=YjzsnFFmu)j76~aOPBw)c2hKcrk6=Aog4M^r%U~73xDuF_bs)@#T7^W|4SJ+NZcb}IQsbvh@zwKN z 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 V*^t4H?)(BSeO)7+a^yq&e*wOXjsXAw literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ea15bb --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# MineSentinel + +``` +🛡️ MineSentinel +Minecraft 服务器守护进程 +``` + +**TCP 端口检测 · 崩溃自动重启 · 冷却期防循环** + +[![License](https://img.shields.io/badge/License-MulanPubL--2.0-orange.svg)](http://license.coscl.org.cn/MulanPubL-2.0) +[![Ruby](https://img.shields.io/badge/Ruby-%3E%3D3.0-cc342d?logo=ruby)](https://www.ruby-lang.org) +[![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20Windows-blue)](#跨场景适配指南) + +## 快速开始 + +```bash +# 复制文件 +mkdir -p /opt/minecraft/sentinel +cp main.rb method_executor.rb server_status_detector.rb /opt/minecraft/sentinel/ + +# 安装 systemd 服务 +cp minesentinel.service /etc/systemd/system/ +systemctl daemon-reload +systemctl enable minesentinel +systemctl start minesentinel + +# 手动运行(调试用) +cd /opt/minecraft/sentinel +ruby main.rb +``` + +## 特性 + +- **高频检测** — 每 1.5 秒 TCP 连接 `127.0.0.1:25565` +- **智能判断** — 端口超时仅记录日志,端口关闭才触发重启 +- **冷却期保护** — 重启后等待 200 秒,防止 MC 启动缓慢导致循环重启 +- **初始宽限期** — MineSentinel 先于 minecraft 启动,自动等待 300 秒至端口就绪 +- **重试上限** — 连续 10 次失败进入空闲等待,端口恢复后自动退出 +- **多场景适配** — 支持 systemd / Docker / screen / Windows + +## 服务启动顺序 + +``` +开机 → network.target + │ + ┌────▼────────┐ + │ MineSentinel │ 最先启动,进入 300s 宽限期等待 MC 就绪 + └────┬────────┘ + │ Before= + ┌────▼────┐ + │minecraft │ 随后启动,TCP 端口打开后 MineSentinel 恢复正常监控 + └─────────┘ +``` + +MineSentinel 通过 `Before=minecraft.service` 确保先于 MC 启动,避免 MC 先启动后崩溃而未被守护的第一时间窗口。首次启动的 300 秒宽限期确保不会把 MC 的正常启动过程误判为崩溃。 + +## 文件说明 + +| 文件 | 职责 | +|------|------| +| `main.rb` | 主循环、冷却期控制、初始宽限期 | +| `method_executor.rb` | 日志系统、systemctl 重启、计数器 | +| `server_status_detector.rb` | TCP 端口检测模块 | +| `minesentinel.service` | systemd 单元文件(独立源码) | + +## 完整文档 + +参见 [DEPLOY.md](DEPLOY.md) 或 [官网](OfficialWebsite/index.html)。 + +## 许可证 + +[MulanPubL v2.0](http://license.coscl.org.cn/MulanPubL-2.0) — Copyright © 2026 S.A. SNOWARE-SCU +> 品牌标识(Logo / 项目名称)保留所有权利,未经许可不得用于衍生项目。 diff --git a/main.rb b/main.rb index 7a61302..8133f81 100644 --- a/main.rb +++ b/main.rb @@ -2,7 +2,15 @@ # usage: MINESENTINEL mainloop. # author: S.A. # time: 2026-06-07 -# license: All rights received. +# Copyright (c) [2026] [S.A. SNOWARE-SCU] +# [MineSentinel] is licensed under Mulan PubL v2. +# You can use this software according to the terms and conditions of the Mulan PubL v2. +# You may obtain a copy of Mulan PubL v2 at: +# http://license.coscl.org.cn/MulanPubL-2.0 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +# See the Mulan PubL v2 for more details. # main.rb 顶部添加 begin @@ -19,6 +27,22 @@ require_relative 'server_status_detector' MethodExecutor.record("info", "mainloop", "MineSentinel started, entering mainloop.") +# 初次启动:MineSentinel 先于 minecraft 启动(systemd Before=), +# 等待服务器端口就绪,避免误判为崩溃 +INITIAL_GRACE = 300 # 初次启动等待 300 秒 +MethodExecutor.record("info", "mainloop", + "Initial startup grace period #{INITIAL_GRACE}s — waiting for server to come up...") +grace_deadline = Time.now + INITIAL_GRACE +while Time.now < grace_deadline + sleep 5 + if ServerStatusDetector::MinecraftServer.self_diag[:type] + MethodExecutor.record("info", "mainloop", + "Server port is open during grace period, resuming normal monitoring.") + break + end +end +MethodExecutor.record("info", "mainloop", "Grace period ended.") + # Functional mainloop. def main chkrslt = ServerStatusDetector::MinecraftServer.self_diag @@ -34,7 +58,7 @@ def main MethodExecutor.record("info", "mainloop", "Action: ForceResetServerProcess triggered.") MethodExecutor.reset_minecraft_server # 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟) - cooldown = 190 # 190秒冷却期 + cooldown = 200 # 200秒冷却期 MethodExecutor.record("info", "mainloop", "Cooling down #{cooldown}s for server startup...") sleep cooldown MethodExecutor.record("info", "mainloop", "Cooldown finished, resuming monitoring.") diff --git a/method_executor.rb b/method_executor.rb index 468b67c..d1603b1 100644 --- a/method_executor.rb +++ b/method_executor.rb @@ -2,7 +2,15 @@ # usage: Automatic operator implementation. # author: S.A. # time: 2026-06-07 -# license: All rights received. +# Copyright (c) [2026] [S.A. SNOWARE-SCU] +# [MineSentinel] is licensed under Mulan PubL v2. +# You can use this software according to the terms and conditions of the Mulan PubL v2. +# You may obtain a copy of Mulan PubL v2 at: +# http://license.coscl.org.cn/MulanPubL-2.0 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +# See the Mulan PubL v2 for more details. # frozen_string_literal: true diff --git a/minesentinel.service b/minesentinel.service new file mode 100644 index 0000000..75af166 --- /dev/null +++ b/minesentinel.service @@ -0,0 +1,27 @@ +# Copyright (c) [2026] [S.A. SNOWARE-SCU] +# [MineSentinel] is licensed under Mulan PubL v2. +# You can use this software according to the terms and conditions of the Mulan PubL v2. +# You may obtain a copy of Mulan PubL v2 at: +# http://license.coscl.org.cn/MulanPubL-2.0 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND. + +[Unit] +Description=MineSentinel Minecraft Server Monitor +After=network.target +Before=minecraft.service +Wants=minecraft.service + +[Service] +Type=simple +WorkingDirectory=/opt/minecraft/sentinel +ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/main.rb +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +#Environment=LOG_LEVEL=debug +#Environment=RUBY_DEBUG=1 +WatchdogSec=210 + +[Install] +WantedBy=multi-user.target diff --git a/server_status_detector.rb b/server_status_detector.rb index fb7a0e8..fc2e2c7 100644 --- a/server_status_detector.rb +++ b/server_status_detector.rb @@ -2,7 +2,15 @@ # usage: MINECRAFT Server monitoring. # author: S.A. # time: 2026-06-07 -# license: All rights received. +# Copyright (c) [2026] [S.A. SNOWARE-SCU] +# [MineSentinel] is licensed under Mulan PubL v2. +# You can use this software according to the terms and conditions of the Mulan PubL v2. +# You may obtain a copy of Mulan PubL v2 at: +# http://license.coscl.org.cn/MulanPubL-2.0 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +# See the Mulan PubL v2 for more details. # frozen_string_literal: true diff --git a/uni_log_path.rb b/uni_log_path.rb deleted file mode 100644 index dac43a2..0000000 --- a/uni_log_path.rb +++ /dev/null @@ -1,21 +0,0 @@ -# <<*>> -^v- SNOWARE APPLICATION -# usage: UNIVERSAL -# author: S.A. -# time: 2026-06-07 -# license: All rights received. - -# CRITICAL! This function decides where the log file will be stored. -def generate_logfile_path( - proc_duty = "MineSentinel", - time_format = "%Y%m%d_%H%M" -) - - # Ensure that destination exists. - result = system "mkdir -p /var/log/snoware" - if not result - raise Errno::EBADEXEC - end - - return "/var/log/snoware" + "/" + proc_duty + "/" + Time.now.strftime(time_format) + ".log" - -end \ No newline at end of file