Compare commits

..

5 Commits

11 changed files with 353 additions and 183 deletions
+6
View File
@@ -0,0 +1,6 @@
AllCops:
NewCops: enable
Layout/EndOfLine:
Enabled: false
Style/Documentation:
Enabled: false
+2
View File
@@ -0,0 +1,2 @@
b43527c
60176fa
+9 -5
View File
@@ -22,7 +22,10 @@ MineSentinel 是一个 Minecraft 服务器守护进程,每 1.5 秒检测服务
/opt/minecraft/sentinel/ # 推荐部署目录 /opt/minecraft/sentinel/ # 推荐部署目录
├── main.rb # 主程序入口 —— 检测循环 + 冷却期 + 初始宽限期 ├── main.rb # 主程序入口 —— 检测循环 + 冷却期 + 初始宽限期
├── method_executor.rb # 日志系统 + 重启逻辑 + 重试计数器 ├── method_executor.rb # 日志系统 + 重启逻辑 + 重试计数器
── 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 单元文件(项目源码中)
``` ```
@@ -35,7 +38,7 @@ minesentinel.service # systemd 单元文件(项目源码中)
```bash ```bash
mkdir -p /opt/minecraft/sentinel mkdir -p /opt/minecraft/sentinel
cp main.rb method_executor.rb server_status_detector.rb /opt/minecraft/sentinel/ cp main.rb method_executor.rb server_status_detector.rb systemd_notifier.rb config.rb updates.rb /opt/minecraft/sentinel/
``` ```
### 2. 确认 Minecraft systemd 服务存在 ### 2. 确认 Minecraft systemd 服务存在
@@ -132,9 +135,10 @@ MineSentinel 启动:
| 参数 | 文件 | 行 | 默认值 | | 参数 | 文件 | 行 | 默认值 |
|------|------|-----|--------| |------|------|-----|--------|
| 初始宽限期 | `main.rb` | 31 | `300` s | | 初始宽限期 | `config.rb` | 1 | `300` s |
| 检测间隔 | `main.rb` | 69 | `1.5` s | | 冷却期 | `config.rb` | 3 | `200` s |
| 冷却期 | `main.rb` | 58 | `200` s | | 尝试次数 | `config.rb` | 2 | `3` |
| 检测间隔 | `config.rb` | 4 | `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` |
+8 -3
View File
@@ -453,6 +453,9 @@
<p class="subtitle">Minecraft 服务器智能守护进程</p> <p class="subtitle">Minecraft 服务器智能守护进程</p>
<p class="tagline">TCP 检测 · 自动重启 · 防循环冷却</p> <p class="tagline">TCP 检测 · 自动重启 · 防循环冷却</p>
<p class="author-badge"><strong>SCU 团队</strong> 拥有 · 编写者 <strong>S.A. [@SNOWARE]</strong></p> <p class="author-badge"><strong>SCU 团队</strong> 拥有 · 编写者 <strong>S.A. [@SNOWARE]</strong></p>
<p style="margin-top:12px;font-size:0.88rem;opacity:0.7;color:#fcd34d;">
当前版本 <code style="background:rgba(255,255,255,0.15);color:#fff;padding:2px 8px;border-radius:4px;">60176fa</code> · 频道 <code style="background:rgba(255,255,255,0.15);color:#fff;padding:2px 8px;border-radius:4px;">master</code>
</p>
</header> </header>
<!-- Features --> <!-- Features -->
@@ -535,6 +538,7 @@
<p style="margin-bottom:6px;">方式二:手动复制</p> <p style="margin-bottom:6px;">方式二:手动复制</p>
<pre>mkdir -p /opt/minecraft/sentinel <pre>mkdir -p /opt/minecraft/sentinel
cp main.rb method_executor.rb server_status_detector.rb \ cp main.rb method_executor.rb server_status_detector.rb \
systemd_notifier.rb config.rb updates.rb \
/opt/minecraft/sentinel/</pre> /opt/minecraft/sentinel/</pre>
<h3>3. 手动测试</h3> <h3>3. 手动测试</h3>
@@ -587,9 +591,10 @@ journalctl -u minesentinel -u minecraft -f # 双服务合并</pre>
<h3>⚙️ 可调参数</h3> <h3>⚙️ 可调参数</h3>
<table> <table>
<tr><th>参数</th><th>文件</th><th></th><th>默认</th></tr> <tr><th>参数</th><th>文件</th><th></th><th>默认</th></tr>
<tr><td>初始宽限期</td><td>main.rb</td><td>31</td><td>300 s</td></tr> <tr><td>初始宽限期</td><td>config.rb</td><td>1</td><td>300 s</td></tr>
<tr><td>检测间隔</td><td>main.rb</td><td>69</td><td>1.5 s</td></tr> <tr><td>冷却期</td><td>config.rb</td><td>3</td><td>200 s</td></tr>
<tr><td>冷却期</td><td>main.rb</td><td>58</td><td>200 s</td></tr> <tr><td>尝试次数</td><td>config.rb</td><td>2</td><td>3 次</td></tr>
<tr><td>检测间隔</td><td>config.rb</td><td>4</td><td>1.5 s</td></tr>
<tr><td>systemctl 超时</td><td>method_executor.rb</td><td>17</td><td>30 s</td></tr> <tr><td>systemctl 超时</td><td>method_executor.rb</td><td>17</td><td>30 s</td></tr>
<tr><td>最大重试</td><td>method_executor.rb</td><td>23</td><td>10 次</td></tr> <tr><td>最大重试</td><td>method_executor.rb</td><td>23</td><td>10 次</td></tr>
<tr><td>检测端口</td><td>server_status_detector.rb</td><td>60</td><td>25565</td></tr> <tr><td>检测端口</td><td>server_status_detector.rb</td><td>60</td><td>25565</td></tr>
+4 -1
View File
@@ -16,7 +16,7 @@ Minecraft 服务器守护进程
```bash ```bash
# 复制文件 # 复制文件
mkdir -p /opt/minecraft/sentinel mkdir -p /opt/minecraft/sentinel
cp main.rb method_executor.rb server_status_detector.rb /opt/minecraft/sentinel/ cp main.rb method_executor.rb server_status_detector.rb systemd_notifier.rb config.rb updates.rb /opt/minecraft/sentinel/
# 安装 systemd 服务 # 安装 systemd 服务
cp minesentinel.service /etc/systemd/system/ cp minesentinel.service /etc/systemd/system/
@@ -61,6 +61,9 @@ 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 单元文件(独立源码) |
## 完整文档 ## 完整文档
+24
View File
@@ -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 秒
+87 -60
View File
@@ -12,86 +12,113 @@
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. # MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PubL v2 for more details. # See the Mulan PubL v2 for more details.
# main.rb 顶部添加 # frozen_string_literal: true
begin
# 原有代码
rescue => e
# 强制输出到stderr
$stderr.puts "[CRITICAL] Unhandled exception: #{e.message}"
$stderr.puts e.backtrace.join("\n")
exit 1
end
puts ' ___ __ ___ ___ ___ ' # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
puts ' |\/| | |\ | |__ /__` |__ |\ | | | |\ | |__ | ' def main
puts ' | | | | \| |___ .__/ |___ | \| | | | \| |___ |___ ' puts ' ___ __ ___ ___ ___ '
puts " MineSentinel dev-rolling version by <*> S.A. [@SNOWARE] and owned by SCU team." puts ' |\/| | |\ | |__ /__` |__ |\ | | | |\ | |__ | '
puts " Copyleft (c) 2026 S.A. SNOWARE-SCU. This program is licensed under Mulan PubL v2." puts ' | | | | \| |___ .__/ |___ | \| | | | \| |___ |___ '
puts " E-mail: SALflake@qq.com | QQ: 758522192 | Website: https://https://swe-iss.rth1.xyz/softwares/minesentinel." puts ' MineSentinel dev-rolling version by <*> S.A. [@SNOWARE] and owned by SCU team.'
puts 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' # 日志系统在此时初始化 require_relative 'method_executor' # 日志系统在此时初始化
MethodExecutor.record("debug", "init", "Initialized MethodExecutor, which implements logging system and intervening functions.") MethodExecutor.record('debug', 'init',
require_relative 'server_status_detector' 'Initialized MethodExecutor, which implements logging system and intervening functions.')
require_relative 'systemd_notifier' # 心跳系统在此时初始化 require_relative 'server_status_detector'
MethodExecutor.record("debug", "init", "Initialized SystemdNotifier, which implements systemd watchdog feeder.") require_relative 'systemd_notifier' # 心跳系统在此时初始化
require_relative 'config'
MethodExecutor.record('debug', 'init', 'Initialized SystemdNotifier, which implements systemd watchdog feeder.')
# 确认是否具有展开干预操作的权限 如果因场景不同不需要请注释这部分内容 # 确认是否具有展开干预操作的权限 如果因场景不同不需要请注释这部分内容
unless Process.euid == 0 unless Process.euid.zero?
MethodExecutor.record("error", "init", "Permission denied: root required to intervene Systemd Services.") MethodExecutor.record('error', 'init', 'Permission denied: root required to intervene Systemd Services.')
raise Errno::EPERM raise Errno::EPERM
end end
MethodExecutor.record('info', 'mainloop', 'MineSentinel started, entering mainloop.')
MethodExecutor.record("info", "mainloop", "MineSentinel started, entering mainloop.") require_relative 'updates'
# 初次启动:MineSentinel 先于 minecraft 启动(systemd Before=), begin
# 等待服务器端口就绪,避免误判为崩溃 upd_result = UpdateManager.check
INITIAL_GRACE = 300 # 初次启动等待 300 秒 if upd_result[:status] == :NewerAvailable
MethodExecutor.record("info", "mainloop", MethodExecutor.record('info', 'updates',
"Initial startup grace period #{INITIAL_GRACE}s — waiting for server to come up...") "Update #{upd_result[:version]} available, please check the git repo for details.")
grace_deadline = Time.now + INITIAL_GRACE end
while Time.now < grace_deadline rescue StandardError => e
sleep 5 MethodExecutor.record('error', 'updates', e.to_s)
if ServerStatusDetector::MinecraftServer.self_diag[:type] end
MethodExecutor.record("info", "mainloop",
"Server port is open during grace period, resuming normal monitoring.") # 初次启动: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
next unless ServerStatusDetector::MinecraftServer.self_diag[:type]
MethodExecutor.record('info', 'mainloop',
'Server port is open during grace period, resuming normal monitoring.')
break break
end end
MethodExecutor.record('info', 'mainloop', 'Grace period ended.')
main_loop
end end
MethodExecutor.record("info", "mainloop", "Grace period ended.")
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
# Functional mainloop. # Functional mainloop.
def main # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
chkrslt = ServerStatusDetector::MinecraftServer.self_diag def main_once
if chkrslt[:type] chkrslts = []
MethodExecutor.record("debug", "mainloop", "Server health check passed.") (0..TRY_TIMES).each do
chkrslt = ServerStatusDetector::MinecraftServer.self_diag
chkrslts.append(chkrslt)
end
if chkrslts.all? { |chkrslt_in| chkrslt_in[:type] }
MethodExecutor.record('debug', 'mainloop', 'Server health check passed.')
else else
MethodExecutor.record("warn", "mainloop", "Server health check FAILED: #{chkrslt[:message]}") chkrslts.each do |result|
for operation in chkrslt[:operation] MethodExecutor.record('warn', 'mainloop', "Server health check FAILED: #{result[:message]}")
if operation == "Record" end
# 消息已在 self_diag 中生成,这里仅记录操作 MethodExecutor.record('info', 'mainloop', "Checked #{TRY_TIMES} time.")
MethodExecutor.record("warn", "mainloop", "Action: Record -> Server died!") if chkrslts.any? { |chkrslt_in| chkrslt_in[:operation].include?('Record') }
elsif operation == "ForceResetServerProcess" MethodExecutor.record('warn', 'mainloop', 'Action: Record -> Server died!')
MethodExecutor.record("info", "mainloop", "Action: ForceResetServerProcess triggered.") if chkrslts.any? { |chkrslt_in| chkrslt_in[:message] == 'Server died for unknown reason, and will be restarted.' }
MethodExecutor.record('info', 'mainloop', 'Action: ForceResetServerProcess triggered.')
MethodExecutor.reset_minecraft_server MethodExecutor.reset_minecraft_server
# 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟) # 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟)
cooldown_duration = 200 # 200秒冷却期
cooldown_beg = Time.now cooldown_beg = Time.now
MethodExecutor.record("info", "mainloop", "Cooling down #{cooldown}s for server startup...") MethodExecutor.record('info', 'mainloop', "Cooling down #{RST_COOLDOWN_DURATION}s for server startup...")
while Time.now < cooldown_beg + cooldown_duration # rubocop:disable Metrics/BlockNesting
while Time.now < cooldown_beg + RST_COOLDOWN_DURATION
sleep 1 sleep 1
SystemdNotifier.notify("WATCHDOG=1") SystemdNotifier.notify('WATCHDOG=1')
end end
MethodExecutor.record("info", "mainloop", "Cooldown finished, resuming monitoring.") # rubocop:enable Metrics/BlockNesting
MethodExecutor.record('info', 'mainloop', 'Cooldown finished, resuming monitoring.')
end end
end end
end end
end end
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
while true def main_loop
sleep 1.5 loop do
SystemdNotifier.notify("WATCHDOG=1") sleep CHECK_INTERVAL
main 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 end
+80 -72
View File
@@ -18,109 +18,117 @@ PERSISTENCE_PERIOD = 30 # Wait for systemctl to take the order.
require 'logger' require 'logger'
require_relative 'server_status_detector' require_relative 'server_status_detector'
# rubocop:disable Style/ClassVars
class MethodExecutor class MethodExecutor
public
RST_COUNTS_LIMIT = 10 RST_COUNTS_LIMIT = 10
@@rst_counter = 0 @@rst_counter = 0
# Initialize the standard log system. # Initialize the standard log system.
@@logger = Logger.new(STDOUT) # This script will be launched as a systemd server. @@logger = Logger.new($stdout) # This script will be launched as a systemd server.
STDOUT.sync = true $stdout.sync = true
STDERR.sync = true $stderr.sync = true
# Fetch the environmental variable and convert it into log level. # Fetch the environmental variable and convert it into log level.
@@logger.level = case ENV["LOG_LEVEL"] # @为单个实例(半生不熟的概念,貌似是在不同的空间独立创建),@@为共享同一个 @@logger.level = case ENV.fetch('LOG_LEVEL', nil) # @为单个实例(半生不熟的概念,貌似是在不同的空间独立创建),@@为共享同一个
when nil # Default config. when nil, 'debug' # Default config.
Logger::Severity::DEBUG Logger::Severity::DEBUG
when "debug" when 'info'
Logger::Severity::DEBUG Logger::Severity::INFO
when "info" when 'warn'
Logger::Severity::INFO Logger::Severity::WARN
when "warn" when 'error'
Logger::Severity::WARN Logger::Severity::ERROR
when "error" else
Logger::Severity::ERROR raise ArgumentError
else
raise ArgumentError
end end
@@logger.progname = "MineSentinel" @@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) def self.record(level, module_name, message)
case level case level
when "info" when 'info'
@@logger.info("#{module_name}: #{message}") @@logger.info("#{module_name}: #{message}")
when "warn" when 'warn'
@@logger.warn("#{module_name}: #{message}") @@logger.warn("#{module_name}: #{message}")
when "error" when 'error'
@@logger.error("#{module_name}: #{message}") @@logger.error("#{module_name}: #{message}")
when "debug" when 'debug'
@@logger.debug("#{module_name}: #{message}") @@logger.debug("#{module_name}: #{message}")
else else
raise ArgumentError raise ArgumentError
end end
end end
def self.reset_minecraft_server # rubocop:enable Metrics/MethodLength
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
self.record("info", "reset", "Attempt ##{@@rst_counter + 1} to reset Minecraft server.") # @return [void]
def self.reset_minecraft_server
record('info', 'reset', "Attempt ##{@@rst_counter + 1} to reset Minecraft server.")
if @@rst_counter >= RST_COUNTS_LIMIT if @@rst_counter >= RST_COUNTS_LIMIT
self.record("error", "reset", record('error', 'reset',
"Tried #{RST_COUNTS_LIMIT} times and failed, now I'll idle until server is manually restarted.") "Tried #{RST_COUNTS_LIMIT} times and failed, now I'll idle until server is manually restarted.")
while true loop do
sleep 10 sleep 10
if ServerStatusDetector::MinecraftServer.self_diag[:type] == true SystemdNotifier.notify('WATCHDOG=1') if defined?(SystemdNotifier)
self.record("info", "reset", "Server recovered! Resuming normal monitoring.") next unless ServerStatusDetector::MinecraftServer.self_diag[:type] == true
@@rst_counter = 0
return record('info', 'reset', 'Server recovered! Resuming normal monitoring.')
end @@rst_counter = 0
return
end end
end end
@@rst_counter += 1 @@rst_counter += 1
unless Process.euid == 0 unless Process.euid.zero?
self.record("error", "reset", "Permission denied: root required to manage systemd services.") record('error', 'reset', 'Permission denied: root required to manage systemd services.')
raise Errno::EPERM raise Errno::EPERM
end end
self.record("info", "reset", "Spawning 'systemctl restart minecraft' (PID will be captured)...") record('info', 'reset', "Spawning 'systemctl restart minecraft' (PID will be captured)...")
pid = spawn "systemctl restart minecraft" pid = spawn 'systemctl restart minecraft'
self.record("debug", "reset", "systemctl restart spawned with PID #{pid}.") record('debug', 'reset', "systemctl restart spawned with PID #{pid}.")
tick_beg = Time.now tick_beg = Time.now
while true # rubocop:disable Metrics/BlockLength
sleep 0.5 loop do
begin sleep 0.5
SystemdNotifier.notify("WATCHDOG=1") SystemdNotifier.notify('WATCHDOG=1')
proc_status = Process.waitpid2(pid, Process::WNOHANG) proc_status = Process.waitpid2(pid, Process::WNOHANG)
if proc_status # 子进程已退出,返回 [pid, status] if proc_status # 子进程已退出,返回 [pid, status]
_, status = proc_status _, status = proc_status
elapsed = (Time.now - tick_beg).round(2) elapsed = (Time.now - tick_beg).round(2)
if status.success? if status.success?
self.record("info", "reset", "'systemctl restart' succeeded in #{elapsed}s.") record('info', 'reset', "'systemctl restart' succeeded in #{elapsed}s.")
@@rst_counter = 0 # 成功时重置计数器 @@rst_counter = 0 # 成功时重置计数器
break else
else record('error', 'reset',
self.record("error", "reset", "'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.") "'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
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
end end
# rubocop:enable Metrics/BlockLength
end
end end
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# rubocop:enable Style/ClassVars
end end
+43 -29
View File
@@ -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 = 10)
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_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
+18 -13
View File
@@ -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_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
# @return [Boolean]
def enabled? def 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 @enable_heartbeat = false
end end
end end
end end
+72
View File
@@ -0,0 +1,72 @@
# <<*>> -^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 = '60176fa'
# @type const CHANNEL: String
CHANNEL = 'master'
# @type const VERSION_LIST_URL: String
VERSION_LIST_URL = "https://raw.giteeusercontent.com/scu_team/minesentinel/#{CHANNEL}/APP_VERSIONS_LIST"
# @type const RECURSIVE_RETRY_DEPTH: Integer
RECURSIVE_RETRY_DEPTH = 3
class UpdateManager
# rubocop:disable Metrics/MethodLength
# @return [String]
def self.fetch
MethodExecutor.record('debug', 'updates', "Fetching version list from #{VERSION_LIST_URL}")
uri = URI(VERSION_LIST_URL)
response = Net::HTTP.get_response(uri)
case response.code.to_i
when 200
MethodExecutor.record('debug', 'updates', 'Version list fetched successfully.')
response.body
else
raise "Unexpected HTTP response code: #{response.code}"
end
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}).")
{ type: :NewerAvailable, version: latest }
end
# rubocop:enable Metrics/MethodLength
end