Compare commits

..

14 Commits

Author SHA1 Message Date
sa 4927bdf019 Update version code 2026-06-08 20:50:54 +08:00
sa 5d2e13db0c fix: updates.rb 处理 Gitee raw URL 302 重定向 2026-06-08 20:31:50 +08:00
sa d494093850 fix: 全面修复5个逻辑问题 2026-06-08 20:21:47 +08:00
sa 5668d6589f Oh my watchdog. 2026-06-08 20:10:58 +08:00
sa 41ef365e25 Adding a heartbeat, avoiding dying cause of one failure. 2026-06-08 19:43:17 +08:00
sa bb3374b5cc Fix ai's doc 2026-06-08 13:55:55 +08:00
sa 28581ea3fd Fix ai's doc 2026-06-08 13:50:07 +08:00
sa 8e46739550 update version serial code 2026-06-08 13:37:07 +08:00
sa 13c4c19add fixed try-times <!> = try-times+1 bug 2026-06-08 13:36:22 +08:00
sa c234b76e78 Fixed bugs. 2026-06-08 13:28:49 +08:00
sa bac58cf3d5 fix: 修复版本号获取 CORS 跨域报错,改用 Gitee API v5 2026-06-08 12:29:53 +08:00
sa 4e71ad293a refactor: 版本号不再硬编码,动态从 APP_VERSIONS_LIST 拉取;所有链接 master → dev 2026-06-08 12:26:45 +08:00
sa 5dabf26fe3 update version code 2026-06-08 12:19:48 +08:00
sa b2baee05ef update version code and update info 2026-06-08 12:19:05 +08:00
10 changed files with 81 additions and 58 deletions
+5 -1
View File
@@ -1,4 +1,8 @@
b43527c b43527c
60176fa 60176fa
8cd942b 8cd942b
83acd30 83acd30
b2baee0
13c4c19
5668d65
d494093
+1 -1
View File
@@ -9,7 +9,7 @@ Minecraft 服务器守护进程
[![License](https://img.shields.io/badge/License-MulanPubL--2.0-orange.svg)](http://license.coscl.org.cn/MulanPubL-2.0) [![License](https://img.shields.io/badge/License-MulanPubL--2.0-orange.svg)](http://license.coscl.org.cn/MulanPubL-2.0)
[![Ruby](https://img.shields.io/badge/Ruby-%3E%3D3.0-cc342d?logo=ruby)](https://www.ruby-lang.org) [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D3.0-cc342d?logo=ruby)](https://www.ruby-lang.org)
[![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20Windows-blue)](#跨场景适配指南) [![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20Windows-blue)](docs/DEPLOY.md#跨场景适配指南)
## 快速开始 ## 快速开始
+7 -7
View File
@@ -136,10 +136,10 @@ MineSentinel 启动:
| 参数 | 文件 | 行 | 默认值 | | 参数 | 文件 | 行 | 默认值 |
|------|------|-----|--------| |------|------|-----|--------|
| 初始宽限期 | `config.rb` | 1 | `300` s | | 初始宽限期 | `config.rb` | 18 | `300` s |
| 冷却期 | `config.rb` | 3 | `200` s | | 冷却期 | `config.rb` | 22 | `200` s |
| 尝试次数 | `config.rb` | 2 | `3` 次 | | 尝试次数 | `config.rb` | 20 | `3` 次 |
| 检测间隔 | `config.rb` | 4 | `1.5` s | | 检测间隔 | `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` |
@@ -204,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` 中的重启命令即可适配。
@@ -222,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 / 项目名称)保留所有权利,未经许可不得用于衍生项目。
+18 -14
View File
@@ -44,7 +44,7 @@ def main
begin begin
upd_result = UpdateManager.check upd_result = UpdateManager.check
if upd_result[:status] == :NewerAvailable if upd_result[:type] == :NewerAvailable
MethodExecutor.record('info', 'updates', MethodExecutor.record('info', 'updates',
"Update #{upd_result[:version]} available, please check the git repo for details.") "Update #{upd_result[:version]} available, please check the git repo for details.")
end end
@@ -59,6 +59,7 @@ def main
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
SystemdNotifier.notify('WATCHDOG=1')
next unless ServerStatusDetector::MinecraftServer.self_diag[:type] next unless ServerStatusDetector::MinecraftServer.self_diag[:type]
MethodExecutor.record('info', 'mainloop', MethodExecutor.record('info', 'mainloop',
@@ -74,32 +75,35 @@ end
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def main_once def main_once
chkrslts = [] chkrslts = []
(0..TRY_TIMES).each do (0...TRY_TIMES).each do
chkrslt = ServerStatusDetector::MinecraftServer.self_diag chkrslt = ServerStatusDetector::MinecraftServer.self_diag
chkrslts.append(chkrslt) chkrslts.append(chkrslt)
sleep 0.5 unless chkrslt[:type] # 只在失败时等待后再重试
end end
if chkrslts.all? { |chkrslt_in| chkrslt_in[:type] } if chkrslts.all? { |chkrslt_in| chkrslt_in[:type] }
MethodExecutor.record('debug', 'mainloop', 'Server health check passed.') MethodExecutor.record('debug', 'mainloop', 'Server health check passed.')
MethodExecutor.reset_rst_counter
else else
chkrslts.each do |result| chkrslts.each do |result|
MethodExecutor.record('warn', 'mainloop', "Server health check FAILED: #{result[:message]}") MethodExecutor.record('warn', 'mainloop', "Server health check FAILED: #{result[:message]}")
end end
MethodExecutor.record('info', 'mainloop', "Checked #{TRY_TIMES} time.") MethodExecutor.record('info', 'mainloop', "Checked #{TRY_TIMES} time.")
if chkrslts.any? { |chkrslt_in| chkrslt_in[:operation].include?('Record') } if chkrslts.any? { |chkrslt_in| (chkrslt_in[:operation] || []).include?('Record') }
MethodExecutor.record('warn', 'mainloop', 'Action: Record -> Server died!') MethodExecutor.record('warn', 'mainloop', 'Action: Record -> Server died!')
if chkrslts.any? { |chkrslt_in| chkrslt_in[:message] == 'Server died for unknown reason, and will be restarted.' } if chkrslts.any? { |chkrslt_in| (chkrslt_in[:operation] || []).include?('ForceResetServerProcess') }
MethodExecutor.record('info', 'mainloop', 'Action: ForceResetServerProcess triggered.') MethodExecutor.record('info', 'mainloop', 'Action: ForceResetServerProcess triggered.')
MethodExecutor.reset_minecraft_server if MethodExecutor.reset_minecraft_server
# 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟) # 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟)
cooldown_beg = Time.now cooldown_beg = Time.now
MethodExecutor.record('info', 'mainloop', "Cooling down #{RST_COOLDOWN_DURATION}s for server startup...") MethodExecutor.record('info', 'mainloop', "Cooling down #{RST_COOLDOWN_DURATION}s for server startup...")
# rubocop:disable Metrics/BlockNesting # rubocop:disable Metrics/BlockNesting
while Time.now < cooldown_beg + RST_COOLDOWN_DURATION while Time.now < cooldown_beg + RST_COOLDOWN_DURATION
sleep 1 sleep 1
SystemdNotifier.notify('WATCHDOG=1') SystemdNotifier.notify('WATCHDOG=1')
end
# rubocop:enable Metrics/BlockNesting
MethodExecutor.record('info', 'mainloop', 'Cooldown finished, resuming monitoring.')
end end
# rubocop:enable Metrics/BlockNesting
MethodExecutor.record('info', 'mainloop', 'Cooldown finished, resuming monitoring.')
end end
end end
end end
+10 -4
View File
@@ -66,13 +66,13 @@ class MethodExecutor
# rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/MethodLength
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# @return [void] # @return [Boolean] true if a restart was attempted, false if recovered from idle
def self.reset_minecraft_server def self.reset_minecraft_server
record('info', 'reset', "Attempt ##{@@rst_counter + 1} to 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
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 recovered.")
loop do loop do
sleep 10 sleep 10
SystemdNotifier.notify('WATCHDOG=1') if defined?(SystemdNotifier) SystemdNotifier.notify('WATCHDOG=1') if defined?(SystemdNotifier)
@@ -80,7 +80,7 @@ class MethodExecutor
record('info', 'reset', 'Server recovered! Resuming normal monitoring.') record('info', 'reset', 'Server recovered! Resuming normal monitoring.')
@@rst_counter = 0 @@rst_counter = 0
return return false
end end
end end
@@ -106,7 +106,6 @@ class MethodExecutor
elapsed = (Time.now - tick_beg).round(2) elapsed = (Time.now - tick_beg).round(2)
if status.success? if status.success?
record('info', 'reset', "'systemctl restart' succeeded in #{elapsed}s.") record('info', 'reset', "'systemctl restart' succeeded in #{elapsed}s.")
@@rst_counter = 0 # 成功时重置计数器
else else
record('error', 'reset', record('error', 'reset',
"'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.") "'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.")
@@ -128,6 +127,13 @@ class MethodExecutor
end end
# rubocop:enable Metrics/BlockLength # rubocop:enable Metrics/BlockLength
end end
true
end
# Resets the restart counter when server health check passes.
# @return [void]
def self.reset_rst_counter
@@rst_counter = 0
end end
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# rubocop:enable Style/ClassVars # rubocop:enable Style/ClassVars
+2 -2
View File
@@ -24,7 +24,7 @@ module ServerStatusDetector
# @param port [Integer] # @param port [Integer]
# @param timeout [Integer] # @param timeout [Integer]
# @return [Hash{Symbol=>Integer, Symbol=>Symbol, Symbol=>Float,nil}] # @return [Hash{Symbol=>Integer, Symbol=>Symbol, Symbol=>Float,nil}]
def self.test_port(host = '127.0.0.1', port = 25_565, timeout = 10) def self.test_port(host = '127.0.0.1', port = 25_565, timeout = 2)
result = { result = {
port: port, port: port,
status: :unknown, status: :unknown,
@@ -61,7 +61,7 @@ module ServerStatusDetector
class MinecraftServer < TcpServer class MinecraftServer < TcpServer
# rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/MethodLength
# @return [Hash{Symbol=>Boolean, Symbol=>Array<String>, Symbol=>String}] # @return [Hash{Symbol=>Boolean, Symbol=>Array<String>, Symbol=>String}]
def self_diag def self.self_diag
res = test_port('127.0.0.1', 25_565) res = test_port('127.0.0.1', 25_565)
unless res[:error].nil? unless res[:error].nil?
+2 -2
View File
@@ -17,6 +17,7 @@
# Sends keep-alive notifications (including WATCHDOG=1) to systemd # Sends keep-alive notifications (including WATCHDOG=1) to systemd
# via the NOTIFY_SOCKET datagram socket, enabling service supervision. # via the NOTIFY_SOCKET datagram socket, enabling service supervision.
class SystemdNotifier class SystemdNotifier
require 'socket'
require_relative 'method_executor' require_relative 'method_executor'
@@ -38,7 +39,7 @@ class SystemdNotifier
end end
# @return [Boolean] # @return [Boolean]
def enabled? def self.enabled?
@enable_heartbeat @enable_heartbeat
end end
@@ -52,7 +53,6 @@ class SystemdNotifier
MethodExecutor.record('debug', 'heartbeat', "Heartbeat sent: #{message}") MethodExecutor.record('debug', 'heartbeat', "Heartbeat sent: #{message}")
rescue StandardError => e rescue StandardError => e
MethodExecutor.record('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
+20 -12
View File
@@ -18,28 +18,35 @@ require 'net/http'
require 'uri' require 'uri'
# @type const RELEASE_SERIAL_CODE: String # @type const RELEASE_SERIAL_CODE: String
RELEASE_SERIAL_CODE = '60176fa' RELEASE_SERIAL_CODE = 'd494093'
# @type const CHANNEL: String # @type const CHANNEL: String
CHANNEL = 'master' CHANNEL = 'dev'
# @type const VERSION_LIST_URL: String # @type const VERSION_LIST_URL: String
VERSION_LIST_URL = "https://raw.giteeusercontent.com/SCU_team/mine-sentinel/#{CHANNEL}/APP_VERSIONS_LIST" VERSION_LIST_URL = "https://gitee.com/SCU_team/mine-sentinel/raw/#{CHANNEL}/APP_VERSIONS_LIST"
# @type const RECURSIVE_RETRY_DEPTH: Integer # @type const RECURSIVE_RETRY_DEPTH: Integer
RECURSIVE_RETRY_DEPTH = 3 RECURSIVE_RETRY_DEPTH = 3
class UpdateManager class UpdateManager
# rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/MethodLength
# @return [String] # @return [String, nil]
def self.fetch def self.fetch
MethodExecutor.record('debug', 'updates', "Fetching version list from #{VERSION_LIST_URL}") MethodExecutor.record('debug', 'updates', "Fetching version list from #{VERSION_LIST_URL}")
uri = URI(VERSION_LIST_URL) uri = URI(VERSION_LIST_URL)
response = Net::HTTP.get_response(uri) 5.times do # 跟随重定向(Gitee raw URL → raw.giteeusercontent.com
case response.code.to_i response = Net::HTTP.get_response(uri)
when 200 case response.code.to_i
MethodExecutor.record('debug', 'updates', 'Version list fetched successfully.') when 200
response.body MethodExecutor.record('debug', 'updates', 'Version list fetched successfully.')
else return response.body
raise "Unexpected HTTP response code: #{response.code}" 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 end
raise 'Too many redirects'
rescue StandardError => e rescue StandardError => e
MethodExecutor.record('error', 'updates', "Failed to fetch version list: #{e}") MethodExecutor.record('error', 'updates', "Failed to fetch version list: #{e}")
nil nil
@@ -65,7 +72,8 @@ class UpdateManager
end end
latest = lines.last latest = lines.last
MethodExecutor.record('info', 'updates', "Newer version #{latest} available (current: #{RELEASE_SERIAL_CODE}).") MethodExecutor.record('info', 'updates',
"Newer version #{latest} available (current: #{RELEASE_SERIAL_CODE}), use git to update.")
{ type: :NewerAvailable, version: latest } { type: :NewerAvailable, version: latest }
end end
# rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/MethodLength
+1 -1
View File
@@ -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/docs/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]]
+15 -14
View File
File diff suppressed because one or more lines are too long