Compare commits
14 Commits
c22feaaf1d
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 4927bdf019 | |||
| 5d2e13db0c | |||
| d494093850 | |||
| 5668d6589f | |||
| 41ef365e25 | |||
| bb3374b5cc | |||
| 28581ea3fd | |||
| 8e46739550 | |||
| 13c4c19add | |||
| c234b76e78 | |||
| bac58cf3d5 | |||
| 4e71ad293a | |||
| 5dabf26fe3 | |||
| b2baee05ef |
+5
-1
@@ -1,4 +1,8 @@
|
||||
b43527c
|
||||
60176fa
|
||||
8cd942b
|
||||
83acd30
|
||||
83acd30
|
||||
b2baee0
|
||||
13c4c19
|
||||
5668d65
|
||||
d494093
|
||||
@@ -9,7 +9,7 @@ Minecraft 服务器守护进程
|
||||
|
||||
[](http://license.coscl.org.cn/MulanPubL-2.0)
|
||||
[](https://www.ruby-lang.org)
|
||||
[](#跨场景适配指南)
|
||||
[](docs/DEPLOY.md#跨场景适配指南)
|
||||
|
||||
## 快速开始
|
||||
|
||||
|
||||
+7
-7
@@ -136,10 +136,10 @@ MineSentinel 启动:
|
||||
|
||||
| 参数 | 文件 | 行 | 默认值 |
|
||||
|------|------|-----|--------|
|
||||
| 初始宽限期 | `config.rb` | 1 | `300` s |
|
||||
| 冷却期 | `config.rb` | 3 | `200` s |
|
||||
| 尝试次数 | `config.rb` | 2 | `3` 次 |
|
||||
| 检测间隔 | `config.rb` | 4 | `1.5` s |
|
||||
| 初始宽限期 | `config.rb` | 18 | `300` s |
|
||||
| 冷却期 | `config.rb` | 22 | `200` s |
|
||||
| 尝试次数 | `config.rb` | 20 | `3` 次 |
|
||||
| 检测间隔 | `config.rb` | 24 | `1.5` s |
|
||||
| systemctl 超时 | `method_executor.rb` | 17 | `30` s |
|
||||
| 最大重试 | `method_executor.rb` | 23 | `10` 次 |
|
||||
| 检测端口 | `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
|
||||
# server_status_detector.rb 第 60 行
|
||||
res = test_port('192.168.1.100', 3306) # MySQL
|
||||
res = test_port('127.0.0.1', 80) # Nginx
|
||||
res = ServerStatusDetector::TcpServer.test_port('192.168.1.100', 3306) # MySQL
|
||||
res = ServerStatusDetector::TcpServer.test_port('127.0.0.1', 80) # Nginx
|
||||
```
|
||||
|
||||
配合修改 `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
@@ -44,7 +44,7 @@ def main
|
||||
|
||||
begin
|
||||
upd_result = UpdateManager.check
|
||||
if upd_result[:status] == :NewerAvailable
|
||||
if upd_result[:type] == :NewerAvailable
|
||||
MethodExecutor.record('info', 'updates',
|
||||
"Update #{upd_result[:version]} available, please check the git repo for details.")
|
||||
end
|
||||
@@ -59,6 +59,7 @@ def main
|
||||
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',
|
||||
@@ -74,32 +75,35 @@ end
|
||||
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
def main_once
|
||||
chkrslts = []
|
||||
(0..TRY_TIMES).each do
|
||||
(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') }
|
||||
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[: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.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')
|
||||
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
|
||||
# rubocop:enable Metrics/BlockNesting
|
||||
MethodExecutor.record('info', 'mainloop', 'Cooldown finished, resuming monitoring.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+10
-4
@@ -66,13 +66,13 @@ class MethodExecutor
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
# 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
|
||||
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 restarted.")
|
||||
"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)
|
||||
@@ -80,7 +80,7 @@ class MethodExecutor
|
||||
|
||||
record('info', 'reset', 'Server recovered! Resuming normal monitoring.')
|
||||
@@rst_counter = 0
|
||||
return
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -106,7 +106,6 @@ class MethodExecutor
|
||||
elapsed = (Time.now - tick_beg).round(2)
|
||||
if status.success?
|
||||
record('info', 'reset', "'systemctl restart' succeeded in #{elapsed}s.")
|
||||
@@rst_counter = 0 # 成功时重置计数器
|
||||
else
|
||||
record('error', 'reset',
|
||||
"'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.")
|
||||
@@ -128,6 +127,13 @@ class MethodExecutor
|
||||
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
|
||||
|
||||
@@ -24,7 +24,7 @@ module ServerStatusDetector
|
||||
# @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)
|
||||
def self.test_port(host = '127.0.0.1', port = 25_565, timeout = 2)
|
||||
result = {
|
||||
port: port,
|
||||
status: :unknown,
|
||||
@@ -61,7 +61,7 @@ module ServerStatusDetector
|
||||
class MinecraftServer < TcpServer
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# @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)
|
||||
|
||||
unless res[:error].nil?
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# Sends keep-alive notifications (including WATCHDOG=1) to systemd
|
||||
# via the NOTIFY_SOCKET datagram socket, enabling service supervision.
|
||||
class SystemdNotifier
|
||||
require 'socket'
|
||||
require_relative 'method_executor'
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ class SystemdNotifier
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
def enabled?
|
||||
def self.enabled?
|
||||
@enable_heartbeat
|
||||
end
|
||||
|
||||
@@ -52,7 +53,6 @@ class SystemdNotifier
|
||||
MethodExecutor.record('debug', 'heartbeat', "Heartbeat sent: #{message}")
|
||||
rescue StandardError => e
|
||||
MethodExecutor.record('error', 'heartbeat', "Failed to send heartbeat: #{e}")
|
||||
@enable_heartbeat = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+20
-12
@@ -18,28 +18,35 @@ require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
# @type const RELEASE_SERIAL_CODE: String
|
||||
RELEASE_SERIAL_CODE = '60176fa'
|
||||
RELEASE_SERIAL_CODE = 'd494093'
|
||||
# @type const CHANNEL: String
|
||||
CHANNEL = 'master'
|
||||
CHANNEL = 'dev'
|
||||
# @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
|
||||
RECURSIVE_RETRY_DEPTH = 3
|
||||
|
||||
class UpdateManager
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# @return [String]
|
||||
# @return [String, nil]
|
||||
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}"
|
||||
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
|
||||
@@ -65,7 +72,8 @@ class UpdateManager
|
||||
end
|
||||
|
||||
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 }
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ name = "MineSentinel"
|
||||
description = "Minecraft 服务器智能守护进程,TCP 端口检测 + 崩溃自动重启 + 防循环冷却期"
|
||||
repo_url = "https://gitee.com/snoware/minesentinel"
|
||||
demo_url = "https://swe-iss.rth1.xyz/softwares/minesentinel"
|
||||
doc_url = "https://gitee.com/snoware/minesentinel/blob/master/docs/DEPLOY.md"
|
||||
doc_url = "https://gitee.com/snoware/minesentinel/blob/dev/docs/DEPLOY.md"
|
||||
tags = ["Ruby", "Minecraft", "Server", "Monitoring", "Daemon", "SNOWARE"]
|
||||
|
||||
[[projects]]
|
||||
|
||||
+15
-14
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user