Compare commits
24 Commits
e7705cab13
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4927bdf019 | |||
| 5d2e13db0c | |||
| d494093850 | |||
| 5668d6589f | |||
| 41ef365e25 | |||
| bb3374b5cc | |||
| 28581ea3fd | |||
| 8e46739550 | |||
| 13c4c19add | |||
| c234b76e78 | |||
| bac58cf3d5 | |||
| 4e71ad293a | |||
| 5dabf26fe3 | |||
| b2baee05ef | |||
| c22feaaf1d | |||
| 83acd3049b | |||
| 8cd942be2d | |||
| 60176fa316 | |||
| b43527c52e | |||
| e421c43a40 | |||
| 3f7ebfb52e | |||
| c99188ef78 | |||
| 72178f8122 | |||
| e9906eb830 |
@@ -0,0 +1,6 @@
|
|||||||
|
AllCops:
|
||||||
|
NewCops: enable
|
||||||
|
Layout/EndOfLine:
|
||||||
|
Enabled: false
|
||||||
|
Style/Documentation:
|
||||||
|
Enabled: false
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
b43527c
|
||||||
|
60176fa
|
||||||
|
8cd942b
|
||||||
|
83acd30
|
||||||
|
b2baee0
|
||||||
|
13c4c19
|
||||||
|
5668d65
|
||||||
|
d494093
|
||||||
@@ -9,23 +9,23 @@ Minecraft 服务器守护进程
|
|||||||
|
|
||||||
[](http://license.coscl.org.cn/MulanPubL-2.0)
|
[](http://license.coscl.org.cn/MulanPubL-2.0)
|
||||||
[](https://www.ruby-lang.org)
|
[](https://www.ruby-lang.org)
|
||||||
[](#跨场景适配指南)
|
[](docs/DEPLOY.md#跨场景适配指南)
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 复制文件
|
# 复制文件
|
||||||
mkdir -p /opt/minecraft/sentinel
|
mkdir -p /opt/minecraft/sentinel/src
|
||||||
cp main.rb method_executor.rb server_status_detector.rb /opt/minecraft/sentinel/
|
cp src/main.rb src/method_executor.rb src/server_status_detector.rb src/systemd_notifier.rb src/config.rb src/updates.rb /opt/minecraft/sentinel/src/
|
||||||
|
|
||||||
# 安装 systemd 服务
|
# 安装 systemd 服务
|
||||||
cp minesentinel.service /etc/systemd/system/
|
cp src/minesentinel.service /etc/systemd/system/
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable minesentinel
|
systemctl enable minesentinel
|
||||||
systemctl start minesentinel
|
systemctl start minesentinel
|
||||||
|
|
||||||
# 手动运行(调试用)
|
# 手动运行(调试用)
|
||||||
cd /opt/minecraft/sentinel
|
cd /opt/minecraft/sentinel/src
|
||||||
ruby main.rb
|
ruby main.rb
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,11 +61,14 @@ MineSentinel 通过 `Before=minecraft.service` 确保先于 MC 启动,避免 M
|
|||||||
| `main.rb` | 主循环、冷却期控制、初始宽限期 |
|
| `main.rb` | 主循环、冷却期控制、初始宽限期 |
|
||||||
| `method_executor.rb` | 日志系统、systemctl 重启、计数器 |
|
| `method_executor.rb` | 日志系统、systemctl 重启、计数器 |
|
||||||
| `server_status_detector.rb` | TCP 端口检测模块 |
|
| `server_status_detector.rb` | TCP 端口检测模块 |
|
||||||
|
| `systemd_notifier.rb` | systemd watchdog 心跳发送 |
|
||||||
|
| `config.rb` | 可调参数配置 |
|
||||||
|
| `updates.rb` | 更新检查模块 |
|
||||||
| `minesentinel.service` | systemd 单元文件(独立源码) |
|
| `minesentinel.service` | systemd 单元文件(独立源码) |
|
||||||
|
|
||||||
## 完整文档
|
## 完整文档
|
||||||
|
|
||||||
参见 [DEPLOY.md](DEPLOY.md) 或 [官网](OfficialWebsite/index.html)。
|
参见 [DEPLOY.md](docs/DEPLOY.md) 或 [官网](static/index.html)。
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
+20
-15
@@ -20,9 +20,13 @@ MineSentinel 是一个 Minecraft 服务器守护进程,每 1.5 秒检测服务
|
|||||||
|
|
||||||
```
|
```
|
||||||
/opt/minecraft/sentinel/ # 推荐部署目录
|
/opt/minecraft/sentinel/ # 推荐部署目录
|
||||||
├── main.rb # 主程序入口 —— 检测循环 + 冷却期 + 初始宽限期
|
└── src/ # 源码目录
|
||||||
├── method_executor.rb # 日志系统 + 重启逻辑 + 重试计数器
|
├── main.rb # 主程序入口 —— 检测循环 + 冷却期 + 初始宽限期
|
||||||
└── server_status_detector.rb # TCP 端口检测模块
|
├── method_executor.rb # 日志系统 + 重启逻辑 + 重试计数器
|
||||||
|
├── server_status_detector.rb # TCP 端口检测模块
|
||||||
|
├── systemd_notifier.rb # systemd watchdog 心跳发送
|
||||||
|
├── config.rb # 可调参数配置
|
||||||
|
└── updates.rb # 更新检查模块
|
||||||
|
|
||||||
minesentinel.service # systemd 单元文件(项目源码中)
|
minesentinel.service # systemd 单元文件(项目源码中)
|
||||||
```
|
```
|
||||||
@@ -34,8 +38,8 @@ minesentinel.service # systemd 单元文件(项目源码中)
|
|||||||
### 1. 创建目录并复制文件
|
### 1. 创建目录并复制文件
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p /opt/minecraft/sentinel
|
mkdir -p /opt/minecraft/sentinel/src
|
||||||
cp main.rb method_executor.rb server_status_detector.rb /opt/minecraft/sentinel/
|
cp src/main.rb src/method_executor.rb src/server_status_detector.rb src/systemd_notifier.rb src/config.rb src/updates.rb /opt/minecraft/sentinel/src/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 确认 Minecraft systemd 服务存在
|
### 2. 确认 Minecraft systemd 服务存在
|
||||||
@@ -49,7 +53,7 @@ systemctl status minecraft
|
|||||||
### 3. 手动测试
|
### 3. 手动测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/minecraft/sentinel
|
cd /opt/minecraft/sentinel/src
|
||||||
ruby main.rb
|
ruby main.rb
|
||||||
# 调试模式:LOG_LEVEL=debug ruby main.rb
|
# 调试模式:LOG_LEVEL=debug ruby main.rb
|
||||||
```
|
```
|
||||||
@@ -59,7 +63,7 @@ ruby main.rb
|
|||||||
项目源码中已提供 `minesentinel.service`,直接复制:
|
项目源码中已提供 `minesentinel.service`,直接复制:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp minesentinel.service /etc/systemd/system/
|
cp src/minesentinel.service /etc/systemd/system/
|
||||||
```
|
```
|
||||||
|
|
||||||
单元文件内容(`minesentinel.service`):
|
单元文件内容(`minesentinel.service`):
|
||||||
@@ -73,8 +77,8 @@ Wants=minecraft.service
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/opt/minecraft/sentinel
|
WorkingDirectory=/opt/minecraft/sentinel/src
|
||||||
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/main.rb
|
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/src/main.rb
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
@@ -132,9 +136,10 @@ MineSentinel 启动:
|
|||||||
|
|
||||||
| 参数 | 文件 | 行 | 默认值 |
|
| 参数 | 文件 | 行 | 默认值 |
|
||||||
|------|------|-----|--------|
|
|------|------|-----|--------|
|
||||||
| 初始宽限期 | `main.rb` | 31 | `300` s |
|
| 初始宽限期 | `config.rb` | 18 | `300` s |
|
||||||
| 检测间隔 | `main.rb` | 69 | `1.5` s |
|
| 冷却期 | `config.rb` | 22 | `200` s |
|
||||||
| 冷却期 | `main.rb` | 58 | `200` s |
|
| 尝试次数 | `config.rb` | 20 | `3` 次 |
|
||||||
|
| 检测间隔 | `config.rb` | 24 | `1.5` s |
|
||||||
| systemctl 超时 | `method_executor.rb` | 17 | `30` s |
|
| systemctl 超时 | `method_executor.rb` | 17 | `30` s |
|
||||||
| 最大重试 | `method_executor.rb` | 23 | `10` 次 |
|
| 最大重试 | `method_executor.rb` | 23 | `10` 次 |
|
||||||
| 检测端口 | `server_status_detector.rb` | 60 | `25565` |
|
| 检测端口 | `server_status_detector.rb` | 60 | `25565` |
|
||||||
@@ -199,8 +204,8 @@ pid = spawn "cmd /c start /B java -Xmx4G -jar C:\\mc\\server.jar nogui"
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# server_status_detector.rb 第 60 行
|
# server_status_detector.rb 第 60 行
|
||||||
res = test_port('192.168.1.100', 3306) # MySQL
|
res = ServerStatusDetector::TcpServer.test_port('192.168.1.100', 3306) # MySQL
|
||||||
res = test_port('127.0.0.1', 80) # Nginx
|
res = ServerStatusDetector::TcpServer.test_port('127.0.0.1', 80) # Nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
配合修改 `method_executor.rb` 中的重启命令即可适配。
|
配合修改 `method_executor.rb` 中的重启命令即可适配。
|
||||||
@@ -217,4 +222,4 @@ res = test_port('127.0.0.1', 80) # Nginx
|
|||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
MineSentinel 源码采用 [木兰宽松许可证 v2](http://license.coscl.org.cn/MulanPubL-2.0)。品牌标识(Logo / 项目名称)保留所有权利,未经许可不得用于衍生项目。
|
MineSentinel 源码采用 [木兰公共许可证 v2](http://license.coscl.org.cn/MulanPubL-2.0)。品牌标识(Logo / 项目名称)保留所有权利,未经许可不得用于衍生项目。
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# <<*>> -^v- SNOWARE APPLICATION
|
|
||||||
# usage: MINESENTINEL mainloop.
|
|
||||||
# author: S.A.
|
|
||||||
# time: 2026-06-07
|
|
||||||
# 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
|
|
||||||
# 原有代码
|
|
||||||
rescue => e
|
|
||||||
# 强制输出到stderr
|
|
||||||
$stderr.puts "[CRITICAL] Unhandled exception: #{e.message}"
|
|
||||||
$stderr.puts e.backtrace.join("\n")
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
puts ' ___ __ ___ ___ ___ '
|
|
||||||
puts ' |\/| | |\ | |__ /__` |__ |\ | | | |\ | |__ | '
|
|
||||||
puts ' | | | | \| |___ .__/ |___ | \| | | | \| |___ |___ '
|
|
||||||
puts " MineSentinel dev-rolling version by <*> S.A. [@SNOWARE] and owned by SCU team."
|
|
||||||
puts " E-mail: SALflake@qq.com | QQ: 758522192 | Website: https://https://swe-iss.rth1.xyz/softwares/minesentinel."
|
|
||||||
puts " Copyleft (c) 2026 S.A. SNOWARE-SCU. This program is licensed under Mulan PubL v2."
|
|
||||||
puts "."
|
|
||||||
|
|
||||||
require_relative 'method_executor' # 日志系统在此时初始化
|
|
||||||
MethodExecutor.record("debug", "init", "Initialized MethodExecutor, which implements logging system and intervening functions.")
|
|
||||||
require_relative 'server_status_detector'
|
|
||||||
require_relative 'systemd_notifier' # 心跳系统在此时初始化
|
|
||||||
MethodExecutor.record("debug", "init", "Initialized SystemdNotifier, which implements systemd watchdog feeder.")
|
|
||||||
|
|
||||||
# 确认是否具有展开干预操作的权限 如果因场景不同不需要请注释这部分内容
|
|
||||||
unless Process.euid == 0
|
|
||||||
MethodExecutor.record("error", "init", "Permission denied: root required to intervene Systemd Services.")
|
|
||||||
raise Errno::EPERM
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
if chkrslt[:type]
|
|
||||||
MethodExecutor.record("debug", "mainloop", "Server health check passed.")
|
|
||||||
else
|
|
||||||
MethodExecutor.record("warn", "mainloop", "Server health check FAILED: #{chkrslt[:message]}")
|
|
||||||
for operation in chkrslt[:operation]
|
|
||||||
if operation == "Record"
|
|
||||||
# 消息已在 self_diag 中生成,这里仅记录操作
|
|
||||||
MethodExecutor.record("warn", "mainloop", "Action: Record -> Server died!")
|
|
||||||
elsif operation == "ForceResetServerProcess"
|
|
||||||
MethodExecutor.record("info", "mainloop", "Action: ForceResetServerProcess triggered.")
|
|
||||||
MethodExecutor.reset_minecraft_server
|
|
||||||
# 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟)
|
|
||||||
cooldown_duration = 200 # 200秒冷却期
|
|
||||||
cooldown_beg = Time.now
|
|
||||||
MethodExecutor.record("info", "mainloop", "Cooling down #{cooldown}s for server startup...")
|
|
||||||
while Time.now < cooldown_beg + cooldown_duration
|
|
||||||
sleep 1
|
|
||||||
SystemdNotifier.notify("WATCHDOG=1")
|
|
||||||
end
|
|
||||||
MethodExecutor.record("info", "mainloop", "Cooldown finished, resuming monitoring.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
while true
|
|
||||||
sleep 1.5
|
|
||||||
SystemdNotifier.notify("WATCHDOG=1")
|
|
||||||
main
|
|
||||||
end
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
# <<*>> -^v- SNOWARE APPLICATION
|
|
||||||
# usage: Automatic operator implementation.
|
|
||||||
# author: S.A.
|
|
||||||
# time: 2026-06-07
|
|
||||||
# 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
|
|
||||||
|
|
||||||
PERSISTENCE_PERIOD = 30 # Wait for systemctl to take the order.
|
|
||||||
|
|
||||||
require 'logger'
|
|
||||||
require_relative 'server_status_detector'
|
|
||||||
class MethodExecutor
|
|
||||||
public
|
|
||||||
RST_COUNTS_LIMIT = 10
|
|
||||||
@@rst_counter = 0
|
|
||||||
# Initialize the standard log system.
|
|
||||||
@@logger = Logger.new(STDOUT) # This script will be launched as a systemd server.
|
|
||||||
STDOUT.sync = true
|
|
||||||
STDERR.sync = true
|
|
||||||
# Fetch the environmental variable and convert it into log level.
|
|
||||||
@@logger.level = case ENV["LOG_LEVEL"] # @为单个实例(半生不熟的概念,貌似是在不同的空间独立创建),@@为共享同一个
|
|
||||||
when nil # Default config.
|
|
||||||
Logger::Severity::DEBUG
|
|
||||||
when "debug"
|
|
||||||
Logger::Severity::DEBUG
|
|
||||||
when "info"
|
|
||||||
Logger::Severity::INFO
|
|
||||||
when "warn"
|
|
||||||
Logger::Severity::WARN
|
|
||||||
when "error"
|
|
||||||
Logger::Severity::ERROR
|
|
||||||
else
|
|
||||||
raise ArgumentError
|
|
||||||
end
|
|
||||||
@@logger.progname = "MineSentinel"
|
|
||||||
|
|
||||||
def self.record(level, module_name, message)
|
|
||||||
case level
|
|
||||||
when "info"
|
|
||||||
@@logger.info("#{module_name}: #{message}")
|
|
||||||
when "warn"
|
|
||||||
@@logger.warn("#{module_name}: #{message}")
|
|
||||||
when "error"
|
|
||||||
@@logger.error("#{module_name}: #{message}")
|
|
||||||
when "debug"
|
|
||||||
@@logger.debug("#{module_name}: #{message}")
|
|
||||||
else
|
|
||||||
raise ArgumentError
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.reset_minecraft_server
|
|
||||||
|
|
||||||
self.record("info", "reset", "Attempt ##{@@rst_counter + 1} to reset Minecraft server.")
|
|
||||||
|
|
||||||
if @@rst_counter >= RST_COUNTS_LIMIT
|
|
||||||
self.record("error", "reset",
|
|
||||||
"Tried #{RST_COUNTS_LIMIT} times and failed, now I'll idle until server is manually restarted.")
|
|
||||||
while true
|
|
||||||
sleep 10
|
|
||||||
if ServerStatusDetector::MinecraftServer.self_diag[:type] == true
|
|
||||||
self.record("info", "reset", "Server recovered! Resuming normal monitoring.")
|
|
||||||
@@rst_counter = 0
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@rst_counter += 1
|
|
||||||
|
|
||||||
unless Process.euid == 0
|
|
||||||
self.record("error", "reset", "Permission denied: root required to manage systemd services.")
|
|
||||||
raise Errno::EPERM
|
|
||||||
end
|
|
||||||
|
|
||||||
self.record("info", "reset", "Spawning 'systemctl restart minecraft' (PID will be captured)...")
|
|
||||||
pid = spawn "systemctl restart minecraft"
|
|
||||||
self.record("debug", "reset", "systemctl restart spawned with PID #{pid}.")
|
|
||||||
tick_beg = Time.now
|
|
||||||
|
|
||||||
while true
|
|
||||||
sleep 0.5
|
|
||||||
begin
|
|
||||||
proc_status = Process.waitpid2(pid, Process::WNOHANG)
|
|
||||||
if proc_status # 子进程已退出,返回 [pid, status]
|
|
||||||
_, status = proc_status
|
|
||||||
elapsed = (Time.now - tick_beg).round(2)
|
|
||||||
if status.success?
|
|
||||||
self.record("info", "reset", "'systemctl restart' succeeded in #{elapsed}s.")
|
|
||||||
@@rst_counter = 0 # 成功时重置计数器
|
|
||||||
break
|
|
||||||
else
|
|
||||||
self.record("error", "reset", "'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.")
|
|
||||||
break
|
|
||||||
end
|
|
||||||
else # 子进程仍在运行 (WNOHANG 返回 nil)
|
|
||||||
if Time.now - tick_beg > PERSISTENCE_PERIOD then
|
|
||||||
self.record("warn", "reset",
|
|
||||||
"'systemctl restart' timed out after #{PERSISTENCE_PERIOD}s. Force-killing PID #{pid}...")
|
|
||||||
system "kill -9 #{pid}"
|
|
||||||
self.record("warn", "reset", "Running 'systemctl kill minecraft' (last resort)...")
|
|
||||||
system "systemctl kill minecraft" # 没招了,毁灭吧
|
|
||||||
sleep 0.5
|
|
||||||
self.record("info", "reset", "Running 'systemctl start minecraft' (rebirth)...")
|
|
||||||
system "systemctl start minecraft" # 重生
|
|
||||||
self.record("info", "reset", "Force-reset sequence completed.")
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# <<*>> -^v- SNOWARE APPLICATION
|
||||||
|
# usage: Configurable constants for MineSentinel.
|
||||||
|
# author: S.A.
|
||||||
|
# time: 2026-06-08
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# @type const INITIAL_GRACE: Integer
|
||||||
|
INITIAL_GRACE = 300 # 初次启动等待 300 秒
|
||||||
|
# @type const TRY_TIMES: Integer
|
||||||
|
TRY_TIMES = 3 # 尝试次数
|
||||||
|
# @type const RST_COOLDOWN_DURATION: Integer
|
||||||
|
RST_COOLDOWN_DURATION = 200 # 重启冷却期 200 秒
|
||||||
|
# @type const CHECK_INTERVAL: Float
|
||||||
|
CHECK_INTERVAL = 1.5 # 检测间隔 1.5 秒
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
# <<*>> -^v- SNOWARE APPLICATION
|
||||||
|
# usage: MINESENTINEL mainloop.
|
||||||
|
# author: S.A.
|
||||||
|
# time: 2026-06-07
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
||||||
|
def main
|
||||||
|
puts ' ___ __ ___ ___ ___ '
|
||||||
|
puts ' |\/| | |\ | |__ /__` |__ |\ | | | |\ | |__ | '
|
||||||
|
puts ' | | | | \| |___ .__/ |___ | \| | | | \| |___ |___ '
|
||||||
|
puts ' MineSentinel dev-rolling version by <*> S.A. [@SNOWARE] and owned by SCU team.'
|
||||||
|
puts ' Copyleft (c) 2026 S.A. SNOWARE-SCU. This program is licensed under Mulan PubL v2.'
|
||||||
|
puts ' E-mail: SALflake@qq.com | QQ: 758522192 | Website: https://swe-iss.rth1.xyz/softwares/minesentinel.'
|
||||||
|
puts
|
||||||
|
|
||||||
|
require_relative 'method_executor' # 日志系统在此时初始化
|
||||||
|
MethodExecutor.record('debug', 'init',
|
||||||
|
'Initialized MethodExecutor, which implements logging system and intervening functions.')
|
||||||
|
require_relative 'server_status_detector'
|
||||||
|
require_relative 'systemd_notifier' # 心跳系统在此时初始化
|
||||||
|
require_relative 'config'
|
||||||
|
MethodExecutor.record('debug', 'init', 'Initialized SystemdNotifier, which implements systemd watchdog feeder.')
|
||||||
|
|
||||||
|
# 确认是否具有展开干预操作的权限 如果因场景不同不需要请注释这部分内容
|
||||||
|
unless Process.euid.zero?
|
||||||
|
MethodExecutor.record('error', 'init', 'Permission denied: root required to intervene Systemd Services.')
|
||||||
|
raise Errno::EPERM
|
||||||
|
end
|
||||||
|
|
||||||
|
MethodExecutor.record('info', 'mainloop', 'MineSentinel started, entering mainloop.')
|
||||||
|
|
||||||
|
require_relative 'updates'
|
||||||
|
|
||||||
|
begin
|
||||||
|
upd_result = UpdateManager.check
|
||||||
|
if upd_result[:type] == :NewerAvailable
|
||||||
|
MethodExecutor.record('info', 'updates',
|
||||||
|
"Update #{upd_result[:version]} available, please check the git repo for details.")
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
MethodExecutor.record('error', 'updates', e.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 初次启动:MineSentinel 先于 minecraft 启动(systemd Before=),
|
||||||
|
# 等待服务器端口就绪,避免误判为崩溃
|
||||||
|
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
|
||||||
|
SystemdNotifier.notify('WATCHDOG=1')
|
||||||
|
next unless ServerStatusDetector::MinecraftServer.self_diag[:type]
|
||||||
|
|
||||||
|
MethodExecutor.record('info', 'mainloop',
|
||||||
|
'Server port is open during grace period, resuming normal monitoring.')
|
||||||
|
break
|
||||||
|
end
|
||||||
|
MethodExecutor.record('info', 'mainloop', 'Grace period ended.')
|
||||||
|
main_loop
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
||||||
|
# Functional mainloop.
|
||||||
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||||
|
def main_once
|
||||||
|
chkrslts = []
|
||||||
|
(0...TRY_TIMES).each do
|
||||||
|
chkrslt = ServerStatusDetector::MinecraftServer.self_diag
|
||||||
|
chkrslts.append(chkrslt)
|
||||||
|
sleep 0.5 unless chkrslt[:type] # 只在失败时等待后再重试
|
||||||
|
end
|
||||||
|
if chkrslts.all? { |chkrslt_in| chkrslt_in[:type] }
|
||||||
|
MethodExecutor.record('debug', 'mainloop', 'Server health check passed.')
|
||||||
|
MethodExecutor.reset_rst_counter
|
||||||
|
else
|
||||||
|
chkrslts.each do |result|
|
||||||
|
MethodExecutor.record('warn', 'mainloop', "Server health check FAILED: #{result[:message]}")
|
||||||
|
end
|
||||||
|
MethodExecutor.record('info', 'mainloop', "Checked #{TRY_TIMES} time.")
|
||||||
|
if chkrslts.any? { |chkrslt_in| (chkrslt_in[:operation] || []).include?('Record') }
|
||||||
|
MethodExecutor.record('warn', 'mainloop', 'Action: Record -> Server died!')
|
||||||
|
if chkrslts.any? { |chkrslt_in| (chkrslt_in[:operation] || []).include?('ForceResetServerProcess') }
|
||||||
|
MethodExecutor.record('info', 'mainloop', 'Action: ForceResetServerProcess triggered.')
|
||||||
|
if MethodExecutor.reset_minecraft_server
|
||||||
|
# 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟)
|
||||||
|
cooldown_beg = Time.now
|
||||||
|
MethodExecutor.record('info', 'mainloop', "Cooling down #{RST_COOLDOWN_DURATION}s for server startup...")
|
||||||
|
# rubocop:disable Metrics/BlockNesting
|
||||||
|
while Time.now < cooldown_beg + RST_COOLDOWN_DURATION
|
||||||
|
sleep 1
|
||||||
|
SystemdNotifier.notify('WATCHDOG=1')
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/BlockNesting
|
||||||
|
MethodExecutor.record('info', 'mainloop', 'Cooldown finished, resuming monitoring.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||||
|
def main_loop
|
||||||
|
loop do
|
||||||
|
sleep CHECK_INTERVAL
|
||||||
|
SystemdNotifier.notify('WATCHDOG=1')
|
||||||
|
main_once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
main
|
||||||
|
rescue StandardError => e
|
||||||
|
# 强制输出到stderr
|
||||||
|
warn "[CRITICAL] Unhandled exception: #{e.message}"
|
||||||
|
warn e.backtrace&.join("\n")
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# <<*>> -^v- SNOWARE APPLICATION
|
||||||
|
# usage: Automatic operator implementation.
|
||||||
|
# author: S.A.
|
||||||
|
# time: 2026-06-07
|
||||||
|
# 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
|
||||||
|
|
||||||
|
PERSISTENCE_PERIOD = 30 # Wait for systemctl to take the order.
|
||||||
|
|
||||||
|
require 'logger'
|
||||||
|
require_relative 'server_status_detector'
|
||||||
|
|
||||||
|
# rubocop:disable Style/ClassVars
|
||||||
|
class MethodExecutor
|
||||||
|
RST_COUNTS_LIMIT = 10
|
||||||
|
@@rst_counter = 0
|
||||||
|
# Initialize the standard log system.
|
||||||
|
@@logger = Logger.new($stdout) # This script will be launched as a systemd server.
|
||||||
|
$stdout.sync = true
|
||||||
|
$stderr.sync = true
|
||||||
|
# Fetch the environmental variable and convert it into log level.
|
||||||
|
@@logger.level = case ENV.fetch('LOG_LEVEL', nil) # @为单个实例(半生不熟的概念,貌似是在不同的空间独立创建),@@为共享同一个
|
||||||
|
when nil, 'debug' # Default config.
|
||||||
|
Logger::Severity::DEBUG
|
||||||
|
when 'info'
|
||||||
|
Logger::Severity::INFO
|
||||||
|
when 'warn'
|
||||||
|
Logger::Severity::WARN
|
||||||
|
when 'error'
|
||||||
|
Logger::Severity::ERROR
|
||||||
|
else
|
||||||
|
raise ArgumentError
|
||||||
|
end
|
||||||
|
@@logger.progname = 'MineSentinel'
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
|
|
||||||
|
# @param level [String] "info" | "warn" | "error" | "debug"
|
||||||
|
# @param module_name [String]
|
||||||
|
# @param message [String]
|
||||||
|
# @return [void]
|
||||||
|
def self.record(level, module_name, message)
|
||||||
|
case level
|
||||||
|
when 'info'
|
||||||
|
@@logger.info("#{module_name}: #{message}")
|
||||||
|
when 'warn'
|
||||||
|
@@logger.warn("#{module_name}: #{message}")
|
||||||
|
when 'error'
|
||||||
|
@@logger.error("#{module_name}: #{message}")
|
||||||
|
when 'debug'
|
||||||
|
@@logger.debug("#{module_name}: #{message}")
|
||||||
|
else
|
||||||
|
raise ArgumentError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||||
|
|
||||||
|
# @return [Boolean] true if a restart was attempted, false if recovered from idle
|
||||||
|
def self.reset_minecraft_server
|
||||||
|
record('info', 'reset', "Attempt ##{@@rst_counter + 1} to reset Minecraft server.")
|
||||||
|
|
||||||
|
if @@rst_counter >= RST_COUNTS_LIMIT
|
||||||
|
record('error', 'reset',
|
||||||
|
"Tried #{RST_COUNTS_LIMIT} times and failed, now I'll idle until server is manually recovered.")
|
||||||
|
loop do
|
||||||
|
sleep 10
|
||||||
|
SystemdNotifier.notify('WATCHDOG=1') if defined?(SystemdNotifier)
|
||||||
|
next unless ServerStatusDetector::MinecraftServer.self_diag[:type] == true
|
||||||
|
|
||||||
|
record('info', 'reset', 'Server recovered! Resuming normal monitoring.')
|
||||||
|
@@rst_counter = 0
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@@rst_counter += 1
|
||||||
|
|
||||||
|
unless Process.euid.zero?
|
||||||
|
record('error', 'reset', 'Permission denied: root required to manage systemd services.')
|
||||||
|
raise Errno::EPERM
|
||||||
|
end
|
||||||
|
|
||||||
|
record('info', 'reset', "Spawning 'systemctl restart minecraft' (PID will be captured)...")
|
||||||
|
pid = spawn 'systemctl restart minecraft'
|
||||||
|
record('debug', 'reset', "systemctl restart spawned with PID #{pid}.")
|
||||||
|
tick_beg = Time.now
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/BlockLength
|
||||||
|
loop do
|
||||||
|
sleep 0.5
|
||||||
|
SystemdNotifier.notify('WATCHDOG=1')
|
||||||
|
proc_status = Process.waitpid2(pid, Process::WNOHANG)
|
||||||
|
if proc_status # 子进程已退出,返回 [pid, status]
|
||||||
|
_, status = proc_status
|
||||||
|
elapsed = (Time.now - tick_beg).round(2)
|
||||||
|
if status.success?
|
||||||
|
record('info', 'reset', "'systemctl restart' succeeded in #{elapsed}s.")
|
||||||
|
else
|
||||||
|
record('error', 'reset',
|
||||||
|
"'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.")
|
||||||
|
end
|
||||||
|
break
|
||||||
|
else # 子进程仍在运行 (WNOHANG 返回 nil)
|
||||||
|
if Time.now - tick_beg > PERSISTENCE_PERIOD
|
||||||
|
record('warn', 'reset',
|
||||||
|
"'systemctl restart' timed out after #{PERSISTENCE_PERIOD}s. Force-killing PID #{pid}...")
|
||||||
|
system "kill -9 #{pid}"
|
||||||
|
record('warn', 'reset', "Running 'systemctl kill minecraft' (last resort)...")
|
||||||
|
system 'systemctl kill minecraft' # 没招了,毁灭吧
|
||||||
|
sleep 0.5
|
||||||
|
record('info', 'reset', "Running 'systemctl start minecraft' (rebirth)...")
|
||||||
|
system 'systemctl start minecraft' # 重生
|
||||||
|
record('info', 'reset', 'Force-reset sequence completed.')
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/BlockLength
|
||||||
|
end
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resets the restart counter when server health check passes.
|
||||||
|
# @return [void]
|
||||||
|
def self.reset_rst_counter
|
||||||
|
@@rst_counter = 0
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||||
|
# rubocop:enable Style/ClassVars
|
||||||
|
end
|
||||||
@@ -13,8 +13,8 @@ Wants=minecraft.service
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/opt/minecraft/sentinel
|
WorkingDirectory=/opt/minecraft/sentinel/src
|
||||||
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/main.rb
|
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/src/main.rb
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
@@ -19,8 +19,12 @@ module ServerStatusDetector
|
|||||||
require 'timeout'
|
require 'timeout'
|
||||||
|
|
||||||
class TcpServer
|
class TcpServer
|
||||||
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
||||||
def self.test_port(host = '127.0.0.1', port = 25565, timeout = 10)
|
# @param host [String]
|
||||||
|
# @param port [Integer]
|
||||||
|
# @param timeout [Integer]
|
||||||
|
# @return [Hash{Symbol=>Integer, Symbol=>Symbol, Symbol=>Float,nil}]
|
||||||
|
def self.test_port(host = '127.0.0.1', port = 25_565, timeout = 2)
|
||||||
result = {
|
result = {
|
||||||
port: port,
|
port: port,
|
||||||
status: :unknown,
|
status: :unknown,
|
||||||
@@ -30,12 +34,11 @@ module ServerStatusDetector
|
|||||||
# 带错误处理的代码需要使用 begin...end
|
# 带错误处理的代码需要使用 begin...end
|
||||||
begin
|
begin
|
||||||
Timeout.timeout(timeout) do # 此方法接收整数和代码块作为参数,并在超时时引发错误
|
Timeout.timeout(timeout) do # 此方法接收整数和代码块作为参数,并在超时时引发错误
|
||||||
socket = TCPSocket.new(host,port)
|
socket = TCPSocket.new(host, port)
|
||||||
result[:status] = :open
|
result[:status] = :open
|
||||||
result[:response_time] = (Time.now - begin_time).round(4)
|
result[:response_time] = (Time.now - begin_time).round(4)
|
||||||
socket.close
|
socket.close
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue Timeout::Error
|
rescue Timeout::Error
|
||||||
result[:status] = :timeout
|
result[:status] = :timeout
|
||||||
rescue Errno::ECONNREFUSED
|
rescue Errno::ECONNREFUSED
|
||||||
@@ -44,57 +47,68 @@ module ServerStatusDetector
|
|||||||
result[:status] = :interrupted
|
result[:status] = :interrupted
|
||||||
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH
|
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH
|
||||||
result[:status] = :unreachable
|
result[:status] = :unreachable
|
||||||
rescue => error
|
rescue StandardError => e
|
||||||
result[:status] = :error
|
result[:status] = :error
|
||||||
result[:error] = error.message
|
result[:error] = e.message
|
||||||
end
|
end
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
||||||
end
|
end
|
||||||
|
|
||||||
class MinecraftServer < TcpServer # 继承为TcpServer编写的属性和方法
|
# 继承为TcpServer编写的属性和方法
|
||||||
|
class MinecraftServer < TcpServer
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
|
# @return [Hash{Symbol=>Boolean, Symbol=>Array<String>, Symbol=>String}]
|
||||||
|
def self.self_diag
|
||||||
|
res = test_port('127.0.0.1', 25_565)
|
||||||
|
|
||||||
def self.self_diag()
|
unless res[:error].nil?
|
||||||
|
|
||||||
res = test_port('127.0.0.1', 25565)
|
|
||||||
|
|
||||||
if res[:error] != nil
|
|
||||||
message = "Unexpected ERROR when testing SMP by TCP: #{res[:status]},INFO: #{res[:error]}"
|
message = "Unexpected ERROR when testing SMP by TCP: #{res[:status]},INFO: #{res[:error]}"
|
||||||
puts message
|
puts message
|
||||||
return {
|
return {
|
||||||
type: false,
|
type: false,
|
||||||
operation: ["Record"],
|
operation: %w[Record],
|
||||||
message: message,
|
message: message
|
||||||
}
|
}
|
||||||
end # 立刻错误处理是好习惯
|
# 立刻错误处理是好习惯
|
||||||
|
end
|
||||||
|
|
||||||
case res[:status]
|
case res[:status]
|
||||||
|
|
||||||
when :open
|
when :open
|
||||||
MethodExecutor.record("debug", "self_diag",
|
MethodExecutor.record('debug', 'self_diag',
|
||||||
"TCP check OK — #{res[:response_time]}s")
|
"TCP check OK — #{res[:response_time]}s")
|
||||||
return {
|
{
|
||||||
type: true,
|
type: true
|
||||||
}
|
}
|
||||||
when :timeout
|
when :timeout
|
||||||
MethodExecutor.record("warn", "self_diag",
|
MethodExecutor.record('warn', 'self_diag',
|
||||||
"TCP check timed out.")
|
'TCP check timed out.')
|
||||||
return {
|
{
|
||||||
type: false,
|
type: false,
|
||||||
operation: ["Record"],
|
operation: %w[Record],
|
||||||
message: "We sent packet without response."
|
message: 'We sent packet without response.'
|
||||||
}
|
}
|
||||||
when :closed
|
when :closed
|
||||||
MethodExecutor.record("warn", "self_diag",
|
MethodExecutor.record('warn', 'self_diag',
|
||||||
"TCP port closed — server is down.")
|
'TCP port closed — server is down.')
|
||||||
return {
|
{
|
||||||
type: false,
|
type: false,
|
||||||
operation: ["Record", "ForceResetServerProcess"], # 重启时注意要设置最大尝试次数
|
operation: %w[Record ForceResetServerProcess], # 重启时注意要设置最大尝试次数
|
||||||
message: "Server died for unknown reason, and will be restarted."
|
message: 'Server died for unknown reason, and will be restarted.'
|
||||||
|
}
|
||||||
|
else
|
||||||
|
MethodExecutor.record('warn', 'self_diag',
|
||||||
|
"TCP check returned unexpected status: #{res[:status]}.")
|
||||||
|
{
|
||||||
|
type: false,
|
||||||
|
operation: %w[Record],
|
||||||
|
message: "Unexpected status: #{res[:status]}."
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -14,40 +14,45 @@
|
|||||||
|
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Sends keep-alive notifications (including WATCHDOG=1) to systemd
|
||||||
|
# via the NOTIFY_SOCKET datagram socket, enabling service supervision.
|
||||||
class SystemdNotifier
|
class SystemdNotifier
|
||||||
|
require 'socket'
|
||||||
require_relative 'method_executor'
|
require_relative 'method_executor'
|
||||||
public
|
|
||||||
|
|
||||||
@notify_socket = ENV["NOTIFY_SOCKET"]
|
|
||||||
@watchdog_usec = (ENV["WATCHDOG_USEC"] || '0').to_i
|
|
||||||
|
|
||||||
unless @notify_socket && File.socket?(@notify_socket)
|
@notify_socket = ENV.fetch('NOTIFY_SOCKET', nil)
|
||||||
MethodExecutor.record("error", "heartbeat", "NOTIFY_SOCKET is not set or is not a socket, am I a service?")
|
@watchdog_usec = (ENV['WATCHDOG_USEC'] || '0').to_i
|
||||||
@enable_heartbeat = false
|
|
||||||
else
|
if @notify_socket && File.socket?(@notify_socket)
|
||||||
MethodExecutor.record("debug", "heartbeat", "Found NOTIFY_SOCKET, enabling heartbeat")
|
MethodExecutor.record('debug', 'heartbeat', 'Found NOTIFY_SOCKET, enabling heartbeat')
|
||||||
@enable_heartbeat = true
|
@enable_heartbeat = true
|
||||||
# Fix(2026-06-08): 将 UNIXSocket(SOCK_STREAM) 改为 SOCK_DGRAM
|
# Fix(2026-06-08): 将 UNIXSocket(SOCK_STREAM) 改为 SOCK_DGRAM
|
||||||
# systemd NOTIFY_SOCKET 是 datagram socket,用 stream socket 连接会报
|
# systemd NOTIFY_SOCKET 是 datagram socket,用 stream socket 连接会报
|
||||||
# Protocol wrong type for socket (Errno::EPROTOTYPE)
|
# Protocol wrong type for socket (Errno::EPROTOTYPE)
|
||||||
@socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
|
@socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
|
||||||
@socket.connect(Socket.pack_sockaddr_un(@notify_socket))
|
@socket.connect(Socket.pack_sockaddr_un(@notify_socket))
|
||||||
MethodExecutor.record("debug", "heartbeat", "Heartbeat socket opened")
|
MethodExecutor.record('debug', 'heartbeat', 'Heartbeat socket opened')
|
||||||
|
else
|
||||||
|
MethodExecutor.record('error', 'heartbeat', 'NOTIFY_SOCKET is not set or is not a socket, am I a service?')
|
||||||
|
@enable_heartbeat = false
|
||||||
end
|
end
|
||||||
|
|
||||||
def enabled?
|
# @return [Boolean]
|
||||||
|
def self.enabled?
|
||||||
@enable_heartbeat
|
@enable_heartbeat
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param message [String]
|
||||||
|
# @return [void]
|
||||||
def self.notify(message)
|
def self.notify(message)
|
||||||
return unless @enable_heartbeat
|
return unless @enable_heartbeat
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@socket.send(message, 0)
|
@socket.send(message, 0)
|
||||||
MethodExecutor.record("debug", "heartbeat", "Heartbeat sent: #{message}")
|
MethodExecutor.record('debug', 'heartbeat', "Heartbeat sent: #{message}")
|
||||||
rescue => e
|
rescue StandardError => e
|
||||||
MethodExecutor.error("heartbeat", "Failed to send heartbeat: #{e}")
|
MethodExecutor.record('error', 'heartbeat', "Failed to send heartbeat: #{e}")
|
||||||
@enable_heartbeat = false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# <<*>> -^v- SNOWARE APPLICATION
|
||||||
|
# usage: Check for updates from remote.
|
||||||
|
# author: S.A.
|
||||||
|
# time: 2026-06-08
|
||||||
|
# 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
|
||||||
|
|
||||||
|
require 'net/http'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
# @type const RELEASE_SERIAL_CODE: String
|
||||||
|
RELEASE_SERIAL_CODE = 'd494093'
|
||||||
|
# @type const CHANNEL: String
|
||||||
|
CHANNEL = 'dev'
|
||||||
|
# @type const VERSION_LIST_URL: String
|
||||||
|
VERSION_LIST_URL = "https://gitee.com/SCU_team/mine-sentinel/raw/#{CHANNEL}/APP_VERSIONS_LIST"
|
||||||
|
# @type const RECURSIVE_RETRY_DEPTH: Integer
|
||||||
|
RECURSIVE_RETRY_DEPTH = 3
|
||||||
|
|
||||||
|
class UpdateManager
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
|
# @return [String, nil]
|
||||||
|
def self.fetch
|
||||||
|
MethodExecutor.record('debug', 'updates', "Fetching version list from #{VERSION_LIST_URL}")
|
||||||
|
uri = URI(VERSION_LIST_URL)
|
||||||
|
5.times do # 跟随重定向(Gitee raw URL → raw.giteeusercontent.com)
|
||||||
|
response = Net::HTTP.get_response(uri)
|
||||||
|
case response.code.to_i
|
||||||
|
when 200
|
||||||
|
MethodExecutor.record('debug', 'updates', 'Version list fetched successfully.')
|
||||||
|
return response.body
|
||||||
|
when 301, 302, 307, 308
|
||||||
|
uri = URI(response['location'])
|
||||||
|
MethodExecutor.record('debug', 'updates', "Following redirect to #{uri}")
|
||||||
|
next
|
||||||
|
else
|
||||||
|
raise "Unexpected HTTP response code: #{response.code}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
raise 'Too many redirects'
|
||||||
|
rescue StandardError => e
|
||||||
|
MethodExecutor.record('error', 'updates', "Failed to fetch version list: #{e}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
|
# @return [Hash{Symbol=>Symbol, Symbol=>String}]
|
||||||
|
def self.check
|
||||||
|
raw = fetch
|
||||||
|
if raw.nil?
|
||||||
|
MethodExecutor.record('error', 'updates', 'Version list is empty, cannot check updates.')
|
||||||
|
raise 'Unknown Error'
|
||||||
|
end
|
||||||
|
|
||||||
|
lines = raw.lines(chomp: true)
|
||||||
|
lines.each_with_index do |line, index|
|
||||||
|
line_num = index + 1
|
||||||
|
if line == RELEASE_SERIAL_CODE && line_num == lines.size
|
||||||
|
MethodExecutor.record('info', 'updates', "Current version #{RELEASE_SERIAL_CODE} is up to date.")
|
||||||
|
return { type: :Newest, version: RELEASE_SERIAL_CODE }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
latest = lines.last
|
||||||
|
MethodExecutor.record('info', 'updates',
|
||||||
|
"Newer version #{latest} available (current: #{RELEASE_SERIAL_CODE}), use git to update.")
|
||||||
|
{ type: :NewerAvailable, version: latest }
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
end
|
||||||
@@ -3,7 +3,7 @@ name = "MineSentinel"
|
|||||||
description = "Minecraft 服务器智能守护进程,TCP 端口检测 + 崩溃自动重启 + 防循环冷却期"
|
description = "Minecraft 服务器智能守护进程,TCP 端口检测 + 崩溃自动重启 + 防循环冷却期"
|
||||||
repo_url = "https://gitee.com/snoware/minesentinel"
|
repo_url = "https://gitee.com/snoware/minesentinel"
|
||||||
demo_url = "https://swe-iss.rth1.xyz/softwares/minesentinel"
|
demo_url = "https://swe-iss.rth1.xyz/softwares/minesentinel"
|
||||||
doc_url = "https://gitee.com/snoware/minesentinel/blob/master/DEPLOY.md"
|
doc_url = "https://gitee.com/snoware/minesentinel/blob/dev/docs/DEPLOY.md"
|
||||||
tags = ["Ruby", "Minecraft", "Server", "Monitoring", "Daemon", "SNOWARE"]
|
tags = ["Ruby", "Minecraft", "Server", "Monitoring", "Daemon", "SNOWARE"]
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user