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 秒
+81 -54
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',
"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...") "Initial startup grace period #{INITIAL_GRACE}s — waiting for server to come up...")
grace_deadline = Time.now + INITIAL_GRACE grace_deadline = Time.now + INITIAL_GRACE
while Time.now < grace_deadline while Time.now < grace_deadline
sleep 5 sleep 5
if ServerStatusDetector::MinecraftServer.self_diag[:type] next unless ServerStatusDetector::MinecraftServer.self_diag[:type]
MethodExecutor.record("info", "mainloop",
"Server port is open during grace period, resuming normal monitoring.") 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
def main_once
chkrslts = []
(0..TRY_TIMES).each do
chkrslt = ServerStatusDetector::MinecraftServer.self_diag chkrslt = ServerStatusDetector::MinecraftServer.self_diag
if chkrslt[:type] chkrslts.append(chkrslt)
MethodExecutor.record("debug", "mainloop", "Server health check passed.") 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
def main_loop
loop do
sleep CHECK_INTERVAL
SystemdNotifier.notify('WATCHDOG=1')
main_once
end
end
while true begin
sleep 1.5
SystemdNotifier.notify("WATCHDOG=1")
main main
rescue StandardError => e
# 强制输出到stderr
warn "[CRITICAL] Unhandled exception: #{e.message}"
warn e.backtrace&.join("\n")
exit 1
end end
+54 -46
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
when "info"
Logger::Severity::INFO Logger::Severity::INFO
when "warn" when 'warn'
Logger::Severity::WARN Logger::Severity::WARN
when "error" when 'error'
Logger::Severity::ERROR Logger::Severity::ERROR
else else
raise ArgumentError 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
record('info', 'reset', 'Server recovered! Resuming normal monitoring.')
@@rst_counter = 0 @@rst_counter = 0
return return
end 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
loop do
sleep 0.5 sleep 0.5
begin 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
self.record("error", "reset", "'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.") record('error', 'reset',
break "'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.")
end end
break
else # 子进程仍在运行 (WNOHANG 返回 nil) else # 子进程仍在运行 (WNOHANG 返回 nil)
if Time.now - tick_beg > PERSISTENCE_PERIOD then if Time.now - tick_beg > PERSISTENCE_PERIOD
self.record("warn", "reset", record('warn', 'reset',
"'systemctl restart' timed out after #{PERSISTENCE_PERIOD}s. Force-killing PID #{pid}...") "'systemctl restart' timed out after #{PERSISTENCE_PERIOD}s. Force-killing PID #{pid}...")
system "kill -9 #{pid}" system "kill -9 #{pid}"
self.record("warn", "reset", "Running 'systemctl kill minecraft' (last resort)...") record('warn', 'reset', "Running 'systemctl kill minecraft' (last resort)...")
system "systemctl kill minecraft" # 没招了,毁灭吧 system 'systemctl kill minecraft' # 没招了,毁灭吧
sleep 0.5 sleep 0.5
self.record("info", "reset", "Running 'systemctl start minecraft' (rebirth)...") record('info', 'reset', "Running 'systemctl start minecraft' (rebirth)...")
system "systemctl start minecraft" # 重生 system 'systemctl start minecraft' # 重生
self.record("info", "reset", "Force-reset sequence completed.") record('info', 'reset', 'Force-reset sequence completed.')
break break
end end
end end
# rubocop:enable Metrics/BlockLength
end end
end end
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# rubocop:enable Style/ClassVars
end
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