dev1.0 版本

This commit is contained in:
2026-06-07 11:05:26 +08:00
commit 777c8b2021
10 changed files with 505 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State>
<id>AccessibilityHTML</id>
</State>
<State>
<id>Code style issuesJavaScript and TypeScript</id>
</State>
<State>
<id>EditorConfig</id>
</State>
<State>
<id>General</id>
</State>
<State>
<id>GeneralJavaScript and TypeScript</id>
</State>
<State>
<id>GitHub actions</id>
</State>
<State>
<id>HTML</id>
</State>
<State>
<id>Imports and dependenciesJavaScript and TypeScript</id>
</State>
<State>
<id>Internationalization</id>
</State>
<State>
<id>JSON and JSON5</id>
</State>
<State>
<id>JavaScript and TypeScript</id>
</State>
<State>
<id>Language level migration aidsRuby</id>
</State>
<State>
<id>Proofreading</id>
</State>
<State>
<id>Ruby</id>
</State>
<State>
<id>SQL</id>
</State>
<State>
<id>Validity issuesJavaScript and TypeScript</id>
</State>
</expanded-state>
<selected-state>
<State>
<id>User defined</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/untitled.iml" filepath="$PROJECT_DIR$/.idea/untitled.iml" />
</modules>
</component>
</project>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RUBY_MODULE" version="4">
<component name="ModuleRunConfigurationManager">
<shared />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
</content>
<orderEntry type="jdk" jdkName="ruby-3.4.9-p82" jdkType="RUBY_SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+140
View File
@@ -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`
+35
View File
@@ -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
+114
View File
@@ -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
+92
View File
@@ -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
+21
View File
@@ -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