diff --git a/APP_VERSIONS_LIST b/APP_VERSIONS_LIST
new file mode 100644
index 0000000..f76f4c1
--- /dev/null
+++ b/APP_VERSIONS_LIST
@@ -0,0 +1 @@
+b43527c
\ No newline at end of file
diff --git a/DEPLOY.md b/DEPLOY.md
index 5a52c07..c1f3391 100644
--- a/DEPLOY.md
+++ b/DEPLOY.md
@@ -24,7 +24,8 @@ MineSentinel 是一个 Minecraft 服务器守护进程,每 1.5 秒检测服务
├── method_executor.rb # 日志系统 + 重启逻辑 + 重试计数器
├── server_status_detector.rb # TCP 端口检测模块
├── systemd_notifier.rb # systemd watchdog 心跳发送
-└── config.rb # 可调参数配置
+├── config.rb # 可调参数配置
+└── updates.rb # 更新检查模块
minesentinel.service # systemd 单元文件(项目源码中)
```
@@ -37,7 +38,7 @@ minesentinel.service # systemd 单元文件(项目源码中)
```bash
mkdir -p /opt/minecraft/sentinel
-cp main.rb method_executor.rb server_status_detector.rb systemd_notifier.rb config.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 服务存在
diff --git a/OfficialWebsite/index.html b/OfficialWebsite/index.html
index ae8dbc7..c8fcd59 100644
--- a/OfficialWebsite/index.html
+++ b/OfficialWebsite/index.html
@@ -535,7 +535,7 @@
方式二:手动复制
mkdir -p /opt/minecraft/sentinel
cp main.rb method_executor.rb server_status_detector.rb \
- systemd_notifier.rb config.rb \
+ systemd_notifier.rb config.rb updates.rb \
/opt/minecraft/sentinel/
3. 手动测试
diff --git a/README.md b/README.md
index 4a3fdf7..c5e3ca8 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Minecraft 服务器守护进程
```bash
# 复制文件
mkdir -p /opt/minecraft/sentinel
-cp main.rb method_executor.rb server_status_detector.rb systemd_notifier.rb config.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 服务
cp minesentinel.service /etc/systemd/system/
@@ -63,6 +63,7 @@ MineSentinel 通过 `Before=minecraft.service` 确保先于 MC 启动,避免 M
| `server_status_detector.rb` | TCP 端口检测模块 |
| `systemd_notifier.rb` | systemd watchdog 心跳发送 |
| `config.rb` | 可调参数配置 |
+| `updates.rb` | 更新检查模块 |
| `minesentinel.service` | systemd 单元文件(独立源码) |
## 完整文档
diff --git a/config.rb b/config.rb
index 4cdad89..e673524 100644
--- a/config.rb
+++ b/config.rb
@@ -1,4 +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 秒
-CHECK_INTERVAL = 1.5 # 检测间隔 1.5 秒
\ No newline at end of file
+# @type const CHECK_INTERVAL: Float
+CHECK_INTERVAL = 1.5 # 检测间隔 1.5 秒
diff --git a/main.rb b/main.rb
index 57f4565..5465e8b 100644
--- a/main.rb
+++ b/main.rb
@@ -36,6 +36,17 @@ def main
MethodExecutor.record("info", "mainloop", "MineSentinel started, entering mainloop.")
+ require_relative 'updates'
+
+ begin
+ upd_result = UpdateManager.check
+ if upd_result[:status] == :NewerAvailable
+ MethodExecutor.record("info", "updates", "Update #{upd_result[:version]} available, please check the git repo for details.")
+ end
+ rescue => check_error
+ MethodExecutor.record("error", "updates", "#{check_error.to_s}")
+ end
+
# 初次启动:MineSentinel 先于 minecraft 启动(systemd Before=),
# 等待服务器端口就绪,避免误判为崩溃
MethodExecutor.record("info", "mainloop",
@@ -56,18 +67,18 @@ end
# Functional mainloop.
def main_once
chkrslts = []
- for i in 0..TRY_TIMES
+ (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
- for chkrslt_in in chkrslts
- MethodExecutor.record("warn", "mainloop", "Server health check FAILED: #{chkrslt_in[:message]}")
+ 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] == "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." }
MethodExecutor.record("info", "mainloop", "Action: ForceResetServerProcess triggered.")
@@ -98,6 +109,6 @@ begin
rescue => e
# 强制输出到stderr
$stderr.puts "[CRITICAL] Unhandled exception: #{e.message}"
- $stderr.puts e.backtrace.join("\n")
+ $stderr.puts e.backtrace&.join("\n")
exit 1
end
diff --git a/method_executor.rb b/method_executor.rb
index 76e9ae5..827ec7f 100644
--- a/method_executor.rb
+++ b/method_executor.rb
@@ -43,6 +43,10 @@ class MethodExecutor
end
@@logger.progname = "MineSentinel"
+ # @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"
@@ -58,6 +62,7 @@ class MethodExecutor
end
end
+ # @return [void]
def self.reset_minecraft_server
self.record("info", "reset", "Attempt ##{@@rst_counter + 1} to reset Minecraft server.")
@@ -67,6 +72,7 @@ class MethodExecutor
"Tried #{RST_COUNTS_LIMIT} times and failed, now I'll idle until server is manually restarted.")
while true
sleep 10
+ SystemdNotifier.notify("WATCHDOG=1") if defined?(SystemdNotifier)
if ServerStatusDetector::MinecraftServer.self_diag[:type] == true
self.record("info", "reset", "Server recovered! Resuming normal monitoring.")
@@rst_counter = 0
diff --git a/server_status_detector.rb b/server_status_detector.rb
index 559ce77..51f3230 100644
--- a/server_status_detector.rb
+++ b/server_status_detector.rb
@@ -20,6 +20,10 @@ module ServerStatusDetector
class TcpServer
+ # @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 = 25565, timeout = 10)
result = {
port: port,
@@ -55,6 +59,7 @@ module ServerStatusDetector
class MinecraftServer < TcpServer # 继承为TcpServer编写的属性和方法
+ # @return [Hash{Symbol=>Boolean, Symbol=>Array, Symbol=>String}]
def self_diag
res = test_port('127.0.0.1', 25565)
@@ -64,7 +69,7 @@ module ServerStatusDetector
puts message
return {
type: false,
- operation: ["Record"],
+ operation: %w[Record],
message: message,
}
end # 立刻错误处理是好习惯
@@ -82,7 +87,7 @@ module ServerStatusDetector
"TCP check timed out.")
return {
type: false,
- operation: ["Record"],
+ operation: %w[Record],
message: "We sent packet without response."
}
when :closed
@@ -90,9 +95,17 @@ module ServerStatusDetector
"TCP port closed — server is down.")
return {
type: false,
- operation: ["Record", "ForceResetServerProcess"], # 重启时注意要设置最大尝试次数
+ operation: %w[Record ForceResetServerProcess], # 重启时注意要设置最大尝试次数
message: "Server died for unknown reason, and will be restarted."
}
+ else
+ MethodExecutor.record("warn", "self_diag",
+ "TCP check returned unexpected status: #{res[:status]}.")
+ return {
+ type: false,
+ operation: %w[Record],
+ message: "Unexpected status: #{res[:status]}."
+ }
end
end
diff --git a/systemd_notifier.rb b/systemd_notifier.rb
index ad14e3e..40cd917 100644
--- a/systemd_notifier.rb
+++ b/systemd_notifier.rb
@@ -35,17 +35,20 @@ class SystemdNotifier
MethodExecutor.record("debug", "heartbeat", "Heartbeat socket opened")
end
+ # @return [Boolean]
def enabled?
@enable_heartbeat
end
+ # @param message [String]
+ # @return [void]
def self.notify(message)
return unless @enable_heartbeat
begin
@socket.send(message, 0)
MethodExecutor.record("debug", "heartbeat", "Heartbeat sent: #{message}")
rescue => e
- MethodExecutor.error("heartbeat", "Failed to send heartbeat: #{e}")
+ MethodExecutor.record("error", "heartbeat", "Failed to send heartbeat: #{e}")
@enable_heartbeat = false
end
end
diff --git a/updates.rb b/updates.rb
new file mode 100644
index 0000000..ea53de1
--- /dev/null
+++ b/updates.rb
@@ -0,0 +1,68 @@
+# <<*>> -^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 = 'b43527c'
+# @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
+ # @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 => e
+ MethodExecutor.record("error", "updates", "Failed to fetch version list: #{e}")
+ nil
+ end
+
+ # @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
+end