dev1.0 版本
This commit is contained in:
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
Generated
+64
@@ -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>
|
||||
Generated
+8
@@ -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>
|
||||
Generated
+15
@@ -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
@@ -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>
|
||||
@@ -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`。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user