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