From 60176fa31661257a4347fedb718453e0a9c38bd3 Mon Sep 17 00:00:00 2001 From: Snowflake Date: Mon, 8 Jun 2026 11:35:51 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BF=AE=E5=A4=8D=E6=BD=9C?= =?UTF-8?q?=E5=9C=A8bug=E3=80=81=E8=A1=A5=E5=85=A8=E8=AE=B8=E5=8F=AF?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E3=80=81=E6=B7=BB=E5=8A=A0=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=B3=A8=E8=A7=A3=E3=80=81=E5=90=8C=E6=AD=A5=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- APP_VERSIONS_LIST | 1 + DEPLOY.md | 5 +-- OfficialWebsite/index.html | 2 +- README.md | 3 +- config.rb | 22 +++++++++++- main.rb | 21 +++++++++--- method_executor.rb | 6 ++++ server_status_detector.rb | 19 +++++++++-- systemd_notifier.rb | 5 ++- updates.rb | 68 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 APP_VERSIONS_LIST create mode 100644 updates.rb 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