commit 777c8b20211880bedea6d5a0cb8d5468356404cb Author: Snowflake Date: Sun Jun 7 11:05:26 2026 +0800 dev1.0 版本 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b6b1ecf --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..03f79cf --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,64 @@ + + + + + + + + AccessibilityHTML + + + Code style issuesJavaScript and TypeScript + + + EditorConfig + + + General + + + GeneralJavaScript and TypeScript + + + GitHub actions + + + HTML + + + Imports and dependenciesJavaScript and TypeScript + + + Internationalization + + + JSON and JSON5 + + + JavaScript and TypeScript + + + Language level migration aidsRuby + + + Proofreading + + + Ruby + + + SQL + + + Validity issuesJavaScript and TypeScript + + + + + User defined + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..aeb7613 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/untitled.iml b/.idea/untitled.iml new file mode 100644 index 0000000..c170610 --- /dev/null +++ b/.idea/untitled.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..ffce661 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,140 @@ +# MineSentinel 部署指南 + +MineSentinel 是一个 Minecraft 服务器守护进程,每 10 秒检测服务器端口状态,发现崩溃时自动重启。 + +## 环境要求 + +- **操作系统**: Linux (systemd) +- **Ruby**: >= 3.0(用到了 `Timeout.timeout` 和 `Process.spawn`) +- **权限**: root(需要执行 `systemctl` 管理 Minecraft 服务) +- **Minecraft 服务**: 已注册为 systemd 服务,服务名为 `minecraft` + +## 文件结构 + +``` +/opt/minesentinel/ # 推荐部署目录 +├── main.rb # 主程序入口 +├── method_executor.rb # 日志系统 + 重启逻辑 +├── server_status_detector.rb # TCP 端口检测 +└── uni_log_path.rb # 日志路径工具(可选) +``` + +## 部署步骤 + +### 1. 创建目录并复制文件 + +```bash +mkdir -p /opt/minesentinel +cp main.rb method_executor.rb server_status_detector.rb /opt/minesentinel/ +``` + +如果要用 `uni_log_path.rb` 也一并复制。 + +### 2. 确认 Minecraft systemd 服务存在 + +```bash +systemctl status minecraft +``` + +如果你的 Minecraft 服务名不是 `minecraft`,需要修改 `method_executor.rb` 中所有 `systemctl xxx minecraft` 为你的实际服务名。 + +### 3. 手动测试 + +先前台跑一次确认无报错: + +```bash +cd /opt/minesentinel +ruby main.rb +``` + +观察日志输出,确认能正常检测端口、识别服务状态。按 `Ctrl+C` 退出。 + +调试模式(输出更详细): + +```bash +LOG_LEVEL=debug ruby main.rb +``` + +### 4. 注册为 systemd 服务 + +创建 `/etc/systemd/system/minesentinel.service`: + +```ini +[Unit] +Description=MineSentinel - Minecraft Server Auto-Restart Daemon +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/minesentinel +ExecStart=/usr/bin/ruby /opt/minesentinel/main.rb +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal +Environment=LOG_LEVEL=info + +[Install] +WantedBy=multi-user.target +``` + +然后: + +```bash +systemctl daemon-reload +systemctl enable minesentinel +systemctl start minesentinel +``` + +### 5. 查看日志 + +```bash +# 实时日志 +journalctl -u minesentinel -f + +# 最近 50 条 +journalctl -u minesentinel -n 50 + +# 只看错误 +journalctl -u minesentinel -p err +``` + +## 日志级别说明 + +通过 `Environment=LOG_LEVEL=xxx` 控制: + +| 级别 | 说明 | +|------|------| +| `debug` | 全部输出,含每次巡检结果、TCP 响应时间 | +| `info` | 启动/重启/恢复等关键操作 | +| `warn` | 服务器异常、超时、强制终止 | +| `error` | 重启失败、重试次数耗尽 | + +默认 `info`,生产环境推荐 `info`;排查问题时临时改为 `debug`。 + +## 工作流程 + +``` +每 10 秒循环: + ├─ TCP 连接 127.0.0.1:25565 + ├─ 端口开放 → debug 日志,继续等待 + ├─ 端口关闭 → 记录 warn,触发重启 + │ └─ systemctl restart minecraft(最多重试 10 次) + │ ├─ 30 秒内完成 → 成功,计数器归零 + │ └─ 超时 → kill -9 → systemctl kill → systemctl start + └─ 10 次全部失败 → 进入空闲等待,直到手动恢复 +``` + +## 常见问题 + +**Q: 报 `Errno::EPERM`?** +A: 需要 root 权限执行。确认 systemd service 以 root 运行,或 `sudo ruby main.rb`。 + +**Q: 如何修改检测端口?** +A: 编辑 `server_status_detector.rb` 第 52 行,修改 `test_port` 的端口参数。 + +**Q: 如何修改检测间隔?** +A: 编辑 `main.rb` 第 33 行,修改 `sleep 10` 的值。 + +**Q: 如何修改重启超时时间?** +A: 编辑 `method_executor.rb` 第 9 行,修改 `PERSISTENCE_PERIOD`。 diff --git a/main.rb b/main.rb new file mode 100644 index 0000000..5df405c --- /dev/null +++ b/main.rb @@ -0,0 +1,35 @@ +# <<*>> -^v- SNOWARE APPLICATION +# usage: MINESENTINEL mainloop. +# author: S.A. +# time: 2026-06-07 +# license: All rights received. + +require_relative 'server_status_detector' +require_relative 'method_executor' # 日志系统在此时初始化 + +MethodExecutor.record("info", "mainloop", "MineSentinel started, entering mainloop.") + +# Functional mainloop. +def main + chkrslt = ServerStatusDetector::MinecraftServer.self_diag + if chkrslt[:type] + MethodExecutor.record("debug", "mainloop", "Server health check passed.") + else + MethodExecutor.record("warn", "mainloop", "Server health check FAILED: #{chkrslt[:message]}") + for operation in chkrslt[:operation] + if operation == "Record" + # 消息已在 self_diag 中生成,这里仅记录操作 + MethodExecutor.record("warn", "mainloop", "Action: Record -> Server died!") + elsif operation == "ForceResetServerProcess" + MethodExecutor.record("info", "mainloop", "Action: ForceResetServerProcess triggered.") + MethodExecutor.reset_minecraft_server + end + end + end +end + + +while true + sleep 10 + main +end diff --git a/method_executor.rb b/method_executor.rb new file mode 100644 index 0000000..c582319 --- /dev/null +++ b/method_executor.rb @@ -0,0 +1,114 @@ +# <<*>> -^v- SNOWARE APPLICATION +# usage: Automatic operator implementation. +# author: S.A. +# time: 2026-06-07 +# license: All rights received. + +# frozen_string_literal: true + +PERSISTENCE_PERIOD = 30 # Wait for systemctl to take the order. + +require 'logger' +require_relative 'server_status_detector' +class MethodExecutor + public + RST_COUNTS_LIMIT = 10 + @@rst_counter = 0 + # Initialize the standard log system. + @@logger = Logger.new(STDOUT) # This script will be launched as a systemd server. + # Fetch the environmental variable and convert it into log level. + @@logger.level = case ENV["LOG_LEVEL"] # @为单个实例(半生不熟的概念,貌似是在不同的空间独立创建),@@为共享同一个 + when nil # Default config. + Logger::Severity::DEBUG + when "debug" + Logger::Severity::DEBUG + when "info" + Logger::Severity::INFO + when "warn" + Logger::Severity::WARN + when "error" + Logger::Severity::ERROR + else + raise ArgumentError + end + @@logger.progname = "MineSentinel" + + def self.record(level, module_name, message) + case level + when "info" + @@logger.info("#{module_name}: #{message}") + when "warn" + @@logger.warn("#{module_name}: #{message}") + when "error" + @@logger.error("#{module_name}: #{message}") + when "debug" + @@logger.debug("#{module_name}: #{message}") + else + raise ArgumentError + end + end + + def self.reset_minecraft_server + + self.record("info", "reset", "Attempt ##{@@rst_counter + 1} to reset Minecraft server.") + + if @@rst_counter >= RST_COUNTS_LIMIT + self.record("error", "reset", + "Tried #{RST_COUNTS_LIMIT} times and failed, now I'll idle until server is manually restarted.") + while true + sleep 10 + if ServerStatusDetector::MinecraftServer.self_diag[:type] == true + self.record("info", "reset", "Server recovered! Resuming normal monitoring.") + @@rst_counter = 0 + return + end + end + end + + @@rst_counter += 1 + + unless Process.euid == 0 + self.record("error", "reset", "Permission denied: root required to manage systemd services.") + raise Errno::EPERM + end + + self.record("info", "reset", "Spawning 'systemctl restart minecraft' (PID will be captured)...") + pid = spawn "systemctl restart minecraft" + self.record("debug", "reset", "systemctl restart spawned with PID #{pid}.") + tick_beg = Time.now + + while true + sleep 0.5 + begin + proc_status = Process.waitpid2(pid, Process::WNOHANG) + if proc_status # 子进程已退出,返回 [pid, status] + _, status = proc_status + elapsed = (Time.now - tick_beg).round(2) + if status.success? + self.record("info", "reset", "'systemctl restart' succeeded in #{elapsed}s.") + @@rst_counter = 0 # 成功时重置计数器 + break + else + self.record("error", "reset", "'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.") + break + end + else # 子进程仍在运行 (WNOHANG 返回 nil) + if Time.now - tick_beg > PERSISTENCE_PERIOD then + self.record("warn", "reset", + "'systemctl restart' timed out after #{PERSISTENCE_PERIOD}s. Force-killing PID #{pid}...") + system "kill -9 #{pid}" + self.record("warn", "reset", "Running 'systemctl kill minecraft' (last resort)...") + system "systemctl kill minecraft" # 没招了,毁灭吧 + sleep 0.5 + self.record("info", "reset", "Running 'systemctl start minecraft' (rebirth)...") + system "systemctl start minecraft" # 重生 + self.record("info", "reset", "Force-reset sequence completed.") + break + end + end + end + end + + + end + end diff --git a/server_status_detector.rb b/server_status_detector.rb new file mode 100644 index 0000000..fb7a0e8 --- /dev/null +++ b/server_status_detector.rb @@ -0,0 +1,92 @@ +# <<*>> -^v- SNOWARE APPLICATION +# usage: MINECRAFT Server monitoring. +# author: S.A. +# time: 2026-06-07 +# license: All rights received. + +# frozen_string_literal: true + +module ServerStatusDetector + require 'socket' + require 'timeout' + + class TcpServer + + def self.test_port(host = '127.0.0.1', port = 25565, timeout = 10) + result = { + port: port, + status: :unknown, + response_time: nil + } + begin_time = Time.now + # 带错误处理的代码需要使用 begin...end + begin + Timeout.timeout(timeout) do # 此方法接收整数和代码块作为参数,并在超时时引发错误 + socket = TCPSocket.new(host,port) + result[:status] = :open + result[:response_time] = (Time.now - begin_time).round(4) + socket.close + end + + rescue Timeout::Error + result[:status] = :timeout + rescue Errno::ECONNREFUSED + result[:status] = :closed + rescue Errno::ECONNRESET + result[:status] = :interrupted + rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH + result[:status] = :unreachable + rescue => error + result[:status] = :error + result[:error] = error.message + end + + result + end + end + + class MinecraftServer < TcpServer # 继承为TcpServer编写的属性和方法 + + def self.self_diag() + + 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]}" + puts message + return { + type: false, + operation: ["Record"], + message: message, + } + end # 立刻错误处理是好习惯 + + case res[:status] + + when :open + MethodExecutor.record("debug", "self_diag", + "TCP check OK — #{res[:response_time]}s") + return { + type: true, + } + when :timeout + MethodExecutor.record("warn", "self_diag", + "TCP check timed out.") + return { + type: false, + operation: ["Record"], + message: "We sent packet without response." + } + when :closed + MethodExecutor.record("warn", "self_diag", + "TCP port closed — server is down.") + return { + type: false, + operation: ["Record", "ForceResetServerProcess"], # 重启时注意要设置最大尝试次数 + message: "Server died for unknown reason, and will be restarted." + } + end + + end + end +end diff --git a/uni_log_path.rb b/uni_log_path.rb new file mode 100644 index 0000000..dac43a2 --- /dev/null +++ b/uni_log_path.rb @@ -0,0 +1,21 @@ +# <<*>> -^v- SNOWARE APPLICATION +# usage: UNIVERSAL +# author: S.A. +# time: 2026-06-07 +# license: All rights received. + +# CRITICAL! This function decides where the log file will be stored. +def generate_logfile_path( + proc_duty = "MineSentinel", + time_format = "%Y%m%d_%H%M" +) + + # Ensure that destination exists. + result = system "mkdir -p /var/log/snoware" + if not result + raise Errno::EBADEXEC + end + + return "/var/log/snoware" + "/" + proc_duty + "/" + Time.now.strftime(time_format) + ".log" + +end \ No newline at end of file