Compare commits
36 Commits
9982a131e9
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4927bdf019 | |||
| 5d2e13db0c | |||
| d494093850 | |||
| 5668d6589f | |||
| 41ef365e25 | |||
| bb3374b5cc | |||
| 28581ea3fd | |||
| 8e46739550 | |||
| 13c4c19add | |||
| c234b76e78 | |||
| bac58cf3d5 | |||
| 4e71ad293a | |||
| 5dabf26fe3 | |||
| b2baee05ef | |||
| c22feaaf1d | |||
| 83acd3049b | |||
| 8cd942be2d | |||
| 60176fa316 | |||
| b43527c52e | |||
| e421c43a40 | |||
| 3f7ebfb52e | |||
| c99188ef78 | |||
| 72178f8122 | |||
| e9906eb830 | |||
| e7705cab13 | |||
| 5b16bbacf8 | |||
| 5d5c5df777 | |||
| 8b59ef2038 | |||
| 4d8500d97d | |||
| 57a157bad7 | |||
| 8fc6c38797 | |||
| 31ef31421e | |||
| 17a2feb043 | |||
| 6311ddee17 | |||
| c61ffe2f75 | |||
| c826d7237c |
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="TypeScriptCompiler">
|
||||
<option name="useService" value="false" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,6 @@
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Layout/EndOfLine:
|
||||
Enabled: false
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
@@ -0,0 +1,8 @@
|
||||
b43527c
|
||||
60176fa
|
||||
8cd942b
|
||||
83acd30
|
||||
b2baee0
|
||||
13c4c19
|
||||
5668d65
|
||||
d494093
|
||||
@@ -1,674 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
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.
|
||||
品牌标识(Logo / SVG / 项目名称" MineSentinel")保留所有权利,未经许可不得用于衍生项目。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MineSentinel — Minecraft 服务器守护进程</title>
|
||||
<script src='https://gitee.com/SCU_team/mine-sentinel/widget_preview' async defer></script>
|
||||
<style>
|
||||
:root {
|
||||
--amber: #f59e0b;
|
||||
--orange: #ea580c;
|
||||
--coral: #e8613c;
|
||||
--warm-bg: #ffedd5;
|
||||
--card-bg: #fff7ed;
|
||||
--text: #431407;
|
||||
--text-dim: #7c2d12;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.7;
|
||||
color: var(--text);
|
||||
background: linear-gradient(160deg, #f97316 0%, #ea580c 40%, #dc2626 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Geometric background elements */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background:
|
||||
repeating-linear-gradient(30deg, transparent, transparent 80px, rgba(255,255,255,0.03) 80px, rgba(255,255,255,0.03) 82px),
|
||||
repeating-linear-gradient(-60deg, transparent, transparent 120px, rgba(255,255,255,0.04) 120px, rgba(255,255,255,0.04) 122px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.geo-hex {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.06;
|
||||
}
|
||||
.geo-hex::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 300px; height: 520px;
|
||||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
||||
background: #fff;
|
||||
}
|
||||
.geo-hex-1 { top: -100px; right: -80px; transform: rotate(15deg); }
|
||||
.geo-hex-2 { bottom: 10%; left: -120px; transform: rotate(-10deg) scale(0.7); }
|
||||
.geo-hex-3 { top: 60%; right: 5%; transform: rotate(45deg) scale(0.4); }
|
||||
|
||||
.geo-tri {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.05;
|
||||
}
|
||||
.geo-tri::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0; height: 0;
|
||||
border-left: 200px solid transparent;
|
||||
border-right: 200px solid transparent;
|
||||
border-bottom: 350px solid #fff;
|
||||
}
|
||||
.geo-tri-1 { top: 30%; left: 5%; transform: rotate(-20deg); }
|
||||
.geo-tri-2 { bottom: 20%; right: 10%; transform: rotate(30deg) scale(0.6); }
|
||||
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 70px 20px 40px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logo-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin: 0 auto 20px;
|
||||
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35));
|
||||
}
|
||||
.logo-svg {
|
||||
width: 100px;
|
||||
display: block;
|
||||
}
|
||||
.logo-png {
|
||||
width: 80px;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
.author-badge {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
margin-top: 4px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.author-badge strong {
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 12px;
|
||||
text-shadow: 0 3px 6px rgba(0,0,0,0.4);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.4rem;
|
||||
opacity: 0.92;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 1rem;
|
||||
letter-spacing: 4px;
|
||||
font-weight: 600;
|
||||
margin-top: 16px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Widget area */
|
||||
.widget-container {
|
||||
margin-bottom: 50px;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
background: rgba(67, 20, 7, 0.85);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||
}
|
||||
.osc_pro_color { color: #ffffff !important; }
|
||||
.osc_panel_color { background-color: #1e252b !important; }
|
||||
.osc_background_color { background-color: #323d47 !important; }
|
||||
.osc_border_color { border-color: #455059 !important; }
|
||||
.osc_desc_color { color: #d7deea !important; }
|
||||
.osc_link_color * { color: #99a0ae !important; }
|
||||
|
||||
/* Log Preview */
|
||||
.log-preview-section {
|
||||
max-width: 860px;
|
||||
margin: 50px auto;
|
||||
padding: 30px;
|
||||
}
|
||||
.log-preview-section h2 {
|
||||
text-align: center;
|
||||
font-size: 1.9rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.log-desc {
|
||||
text-align: center;
|
||||
color: var(--warm-bg);
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.log-terminal {
|
||||
background: #1a1b26;
|
||||
border: 1px solid #323d47;
|
||||
border-radius: 12px;
|
||||
padding: 18px 22px;
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.75;
|
||||
box-shadow: inset 0 2px 12px rgba(0,0,0,0.5), 0 4px 16px rgba(0,0,0,0.25);
|
||||
}
|
||||
.log-line {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #d7deea;
|
||||
}
|
||||
.log-time {
|
||||
color: #565f89;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.log-level {
|
||||
display: inline-block;
|
||||
min-width: 48px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.log-level.info { color: #a9b665; }
|
||||
.log-level.warn { color: #e0af68; }
|
||||
.log-level.debug { color: #7dcfff; }
|
||||
.log-level.error { color: #f7768e; }
|
||||
|
||||
/* Cards */
|
||||
.section-title {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
margin: 50px 0 30px;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,0.18);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
border-top: 4px solid var(--orange);
|
||||
}
|
||||
.feature-card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 14px 36px rgba(0,0,0,0.25);
|
||||
}
|
||||
.feature-card h3 {
|
||||
color: var(--orange);
|
||||
font-size: 1.35rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.feature-card p {
|
||||
color: var(--text-dim);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Deploy tutorial */
|
||||
.deploy-section {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,0.18);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.deploy-section h2 {
|
||||
color: var(--orange);
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 3px solid var(--amber);
|
||||
}
|
||||
.deploy-section h3 {
|
||||
color: var(--text);
|
||||
margin: 24px 0 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.deploy-section h4 {
|
||||
color: var(--orange);
|
||||
margin: 18px 0 8px;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.deploy-section pre {
|
||||
background: #1c1917;
|
||||
color: #fcd34d;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
margin: 10px 0 16px;
|
||||
}
|
||||
.deploy-section code {
|
||||
background: #fff7ed;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--orange);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
.deploy-section table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0 20px;
|
||||
}
|
||||
.deploy-section th, .deploy-section td {
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #fed7aa;
|
||||
}
|
||||
.deploy-section th {
|
||||
background: #ffedd5;
|
||||
color: var(--orange);
|
||||
font-weight: 600;
|
||||
}
|
||||
.deploy-section ul { margin: 8px 0 16px 20px; }
|
||||
.deploy-section li { margin: 4px 0; color: var(--text-dim); }
|
||||
|
||||
/* Flow diagram */
|
||||
.flow-box {
|
||||
background: #1c1917;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
color: #fcd34d;
|
||||
font-family: monospace;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.9;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.cta-section {
|
||||
text-align: center;
|
||||
margin: 50px 0;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 14px 36px;
|
||||
background: white;
|
||||
color: var(--orange);
|
||||
text-decoration: none;
|
||||
border-radius: 30px;
|
||||
font-weight: bold;
|
||||
font-size: 1.05rem;
|
||||
margin: 8px;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.2);
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 8px 22px rgba(0,0,0,0.35);
|
||||
}
|
||||
.btn-primary {
|
||||
background: #f97316;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover { background: #ea580c; }
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.modal.show { display: flex; }
|
||||
.modal-content {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 36px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
position: relative;
|
||||
animation: modalIn 0.3s ease-out;
|
||||
}
|
||||
@keyframes modalIn {
|
||||
from { transform: translateY(-40px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 12px; right: 18px;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
color: #9a3412;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.modal-close:hover { color: #431407; }
|
||||
.modal-title { color: var(--orange); font-size: 1.6rem; margin-bottom: 18px; }
|
||||
.modal-body { color: var(--text-dim); line-height: 1.8; margin-bottom: 20px; }
|
||||
.download-info {
|
||||
background: #ffedd5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--orange);
|
||||
}
|
||||
.download-link { color: var(--orange); text-decoration: none; font-weight: bold; }
|
||||
.download-link:hover { text-decoration: underline; }
|
||||
.password-info {
|
||||
margin-top: 14px;
|
||||
padding: 10px;
|
||||
background: #fff3cd;
|
||||
border-radius: 6px;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
margin-top: 40px;
|
||||
}
|
||||
.links { margin-top: 14px; }
|
||||
.links a {
|
||||
color: white; text-decoration: none; margin: 0 14px; opacity: 0.9;
|
||||
}
|
||||
.links a:hover { opacity: 1; text-decoration: underline; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
h1 { font-size: 2rem; }
|
||||
.deploy-section { padding: 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Geometric decorations -->
|
||||
<div class="geo-hex geo-hex-1"></div>
|
||||
<div class="geo-hex geo-hex-2"></div>
|
||||
<div class="geo-hex geo-hex-3"></div>
|
||||
<div class="geo-tri geo-tri-1"></div>
|
||||
<div class="geo-tri geo-tri-2"></div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="logo-row">
|
||||
<img id="logoPng" class="logo-png" src="" alt="SW Security">
|
||||
<svg class="logo-svg" viewBox="0 0 100 80" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0,1 H100" stroke="#ffffff" stroke-width="6" fill="none" opacity="0.9"/>
|
||||
<path d="M0,20 H30" stroke="#ffffff" stroke-width="6" fill="none" opacity="0.9"/>
|
||||
<path d="M0,40 H30 L40,20 L60,60 L70,40 H100" stroke="#ffffff" stroke-width="6" fill="none" opacity="0.9"/>
|
||||
<path d="M70,60 H100" stroke="#ffffff" stroke-width="6" fill="none" opacity="0.9"/>
|
||||
<path d="M0,79 H100" stroke="#ffffff" stroke-width="6" fill="none" opacity="0.9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>🛡️ MineSentinel</h1>
|
||||
<p class="subtitle">Minecraft 服务器智能守护进程</p>
|
||||
<p class="tagline">TCP 检测 · 自动重启 · 防循环冷却</p>
|
||||
<p class="author-badge">由 <strong>SCU 团队</strong> 拥有 · 编写者 <strong>S.A. [@SNOWARE]</strong></p>
|
||||
</header>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<h3>🔍 高频检测</h3>
|
||||
<p>每 1.5 秒对 <code>127.0.0.1:25565</code> 发起 TCP 连接,毫秒级感知服务器状态变化。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🧠 智能判断</h3>
|
||||
<p>严格区分端口超时与端口关闭——超时仅记录日志(避免网络抖动误判),关闭才触发重启。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>⏳ 冷却期保护</h3>
|
||||
<p>重启后等待 200 秒,初次启动有 300 秒宽限期,彻底防止 MC 启动缓慢导致循环重启。Watchdog 同步适配(210s)。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🛡️ 重试上限</h3>
|
||||
<p>连续 10 次重启失败后进入空闲等待模式,每 10 秒检测端口,恢复后自动退出。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🌍 跨平台</h3>
|
||||
<p>核心逻辑纯 Ruby 实现,支持 systemd / Docker / screen / Windows,轻松适配任意进程管理器。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>📝 日志完备</h3>
|
||||
<p>四级日志(debug / info / warn / error),输出到 systemd journal,排查问题一目了然。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Preview -->
|
||||
<div class="log-preview-section">
|
||||
<h2>📋 日志示例</h2>
|
||||
<p class="log-desc">通过 <code>journalctl -u minesentinel -f</code> 实时查看,以下为一次完整崩溃恢复过程的日志输出:</p>
|
||||
<div class="log-terminal">
|
||||
<div class="log-line"><span class="log-time">18:16:17</span><span class="log-level debug">DEBUG</span> self_diag: TCP check OK — 0.0002s</div>
|
||||
<div class="log-line"><span class="log-time">18:16:17</span><span class="log-level debug">DEBUG</span> mainloop: Server health check passed.</div>
|
||||
<div class="log-line"><span class="log-time">18:16:18</span><span class="log-level debug">DEBUG</span> self_diag: TCP check OK — 0.0003s</div>
|
||||
<div class="log-line"><span class="log-time">18:16:18</span><span class="log-level debug">DEBUG</span> mainloop: Server health check passed.</div>
|
||||
<div class="log-line"><span class="log-time">18:16:20</span><span class="log-level warn">WARN</span> self_diag: TCP port closed — server is down.</div>
|
||||
<div class="log-line"><span class="log-time">18:16:21</span><span class="log-level warn">WARN</span> mainloop: Server health check FAILED: Server died for unknown reason, and will be restarted.</div>
|
||||
<div class="log-line"><span class="log-time">18:16:21</span><span class="log-level warn">WARN</span> mainloop: Action: Record → Server died!</div>
|
||||
<div class="log-line"><span class="log-time">18:16:21</span><span class="log-level info">INFO</span> mainloop: Action: ForceResetServerProcess triggered.</div>
|
||||
<div class="log-line"><span class="log-time">18:16:21</span><span class="log-level info">INFO</span> reset: Attempt #1 to reset Minecraft server.</div>
|
||||
<div class="log-line"><span class="log-time">18:16:21</span><span class="log-level info">INFO</span> reset: Spawning 'systemctl restart minecraft' (PID will be captured)...</div>
|
||||
<div class="log-line"><span class="log-time">18:16:21</span><span class="log-level debug">DEBUG</span> reset: systemctl restart spawned with new PID.</div>
|
||||
<div class="log-line"><span class="log-time">18:16:51</span><span class="log-level warn">WARN</span> reset: 'systemctl restart' timed out after 30s. Force-killing process...</div>
|
||||
<div class="log-line"><span class="log-time">18:16:51</span><span class="log-level warn">WARN</span> reset: Running 'systemctl kill minecraft' (last resort)...</div>
|
||||
<div class="log-line"><span class="log-time">18:16:52</span><span class="log-level info">INFO</span> reset: Running 'systemctl start minecraft' (rebirth)...</div>
|
||||
<div class="log-line"><span class="log-time">18:16:52</span><span class="log-level info">INFO</span> reset: Force-reset sequence completed.</div>
|
||||
<div class="log-line"><span class="log-time">18:16:52</span><span class="log-level info">INFO</span> mainloop: Cooling down 200s for server startup...</div>
|
||||
<div class="log-line" style="opacity:.5;"><span class="log-time">... ... ...</span></div>
|
||||
<div class="log-line"><span class="log-time">18:20:12</span><span class="log-level info">INFO</span> mainloop: Cooldown finished, resuming monitoring.</div>
|
||||
<div class="log-line"><span class="log-time">18:20:14</span><span class="log-level debug">DEBUG</span> self_diag: TCP check OK — 0.0002s</div>
|
||||
<div class="log-line"><span class="log-time">18:20:15</span><span class="log-level debug">DEBUG</span> mainloop: Server health check passed.</div>
|
||||
</div>
|
||||
<p class="log-note" style="margin-top:14px;color:var(--text-dim);font-size:0.88em;">💡 日志中 PID、精确时间戳等敏感信息已脱敏。实际输出格式与 <code>journald</code> 一致,支持 <code>--since</code>、<code>-f</code> 等标准过滤参数。</p>
|
||||
</div>
|
||||
|
||||
<!-- Deploy Tutorial -->
|
||||
<div class="deploy-section">
|
||||
<h2>📦 部署教程</h2>
|
||||
|
||||
<h3>1. 环境要求</h3>
|
||||
<table>
|
||||
<tr><th>项目</th><th>说明</th></tr>
|
||||
<tr><td>操作系统</td><td>Linux(需 systemd),也支持 Windows</td></tr>
|
||||
<tr><td>Ruby</td><td>>= 3.0</td></tr>
|
||||
<tr><td>权限</td><td>root(systemctl 需要)</td></tr>
|
||||
<tr><td>MC 服务</td><td>已注册为 systemd 服务,默认名 <code>minecraft</code></td></tr>
|
||||
</table>
|
||||
|
||||
<h3>2. 获取源码</h3>
|
||||
<p style="margin-bottom:6px;">方式一:Git 克隆(推荐,仅拉取必要文件,排除仓库冗余)</p>
|
||||
<pre>git clone --depth 1 --single-branch \
|
||||
https://gitee.com/SCU_team/mine-sentinel.git \
|
||||
/opt/minecraft/sentinel
|
||||
# --depth 1 仅拉取最新提交,不下载历史记录
|
||||
# --single-branch 仅拉取 master 分支</pre>
|
||||
<p style="margin-bottom:6px;">方式二:手动复制</p>
|
||||
<pre>mkdir -p /opt/minecraft/sentinel
|
||||
cp main.rb method_executor.rb server_status_detector.rb \
|
||||
/opt/minecraft/sentinel/</pre>
|
||||
|
||||
<h3>3. 手动测试</h3>
|
||||
<pre>cd /opt/minecraft/sentinel
|
||||
ruby main.rb
|
||||
# 调试模式: LOG_LEVEL=debug ruby main.rb</pre>
|
||||
|
||||
<h3>4. 注册 systemd 服务</h3>
|
||||
<p>项目源码中已提供 <code>minesentinel.service</code>,直接复制:</p>
|
||||
<pre>cp minesentinel.service /etc/systemd/system/</pre>
|
||||
<p>单元文件内容:</p>
|
||||
<pre>[Unit]
|
||||
Description=MineSentinel Minecraft Server Monitor
|
||||
After=network.target
|
||||
Before=minecraft.service
|
||||
Wants=minecraft.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/minecraft/sentinel
|
||||
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/main.rb
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
#Environment=LOG_LEVEL=debug
|
||||
WatchdogSec=210
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target</pre>
|
||||
|
||||
<h4>🔗 启动顺序</h4>
|
||||
<p style="margin-bottom:10px;">MineSentinel 通过 <code>Before=minecraft.service</code> 确保先于 MC 启动,消除守护盲区。首次启动有 <strong>300 秒宽限期</strong>等待端口就绪:</p>
|
||||
<div class="flow-box">开机 → network.target
|
||||
│
|
||||
├─ MineSentinel 启动(最先)
|
||||
│ └─ 进入 300s 宽限期,等待 MC 端口就绪
|
||||
│
|
||||
└─ minecraft 随后启动(Before=)
|
||||
└─ 端口打开 → MineSentinel 恢复正常监控</div>
|
||||
|
||||
<pre>systemctl daemon-reload
|
||||
systemctl enable minesentinel # 同时触发 minecraft(Wants=)
|
||||
systemctl start minesentinel</pre>
|
||||
|
||||
<h3>5. 查看日志</h3>
|
||||
<pre>journalctl -u minesentinel -f # 实时
|
||||
journalctl -u minesentinel -u minecraft -f # 双服务合并</pre>
|
||||
|
||||
<h3>⚙️ 可调参数</h3>
|
||||
<table>
|
||||
<tr><th>参数</th><th>文件</th><th>行</th><th>默认</th></tr>
|
||||
<tr><td>初始宽限期</td><td>main.rb</td><td>31</td><td>300 s</td></tr>
|
||||
<tr><td>检测间隔</td><td>main.rb</td><td>69</td><td>1.5 s</td></tr>
|
||||
<tr><td>冷却期</td><td>main.rb</td><td>58</td><td>200 s</td></tr>
|
||||
<tr><td>systemctl 超时</td><td>method_executor.rb</td><td>17</td><td>30 s</td></tr>
|
||||
<tr><td>最大重试</td><td>method_executor.rb</td><td>23</td><td>10 次</td></tr>
|
||||
<tr><td>检测端口</td><td>server_status_detector.rb</td><td>60</td><td>25565</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>🔄 工作流程</h3>
|
||||
<div class="flow-box">MineSentinel 启动:
|
||||
└─ 300s 宽限期 → 等待 MC 端口就绪(避免误判)
|
||||
|
||||
每 1.5 秒循环:
|
||||
├─ TCP 连接 127.0.0.1:25565
|
||||
├─ 端口开放 → 继续循环
|
||||
├─ 端口超时 → 仅记录日志(不重启)
|
||||
└─ 端口关闭 → 触发重启 → 冷却 200 秒
|
||||
└─ 连续 10 次失败 → 空闲等待</div>
|
||||
|
||||
<h3>🪟 Windows / Docker / 其他场景</h3>
|
||||
<ul>
|
||||
<li><strong>Docker:</strong>替换重启命令为 <code>spawn "docker restart mc-server"</code></li>
|
||||
<li><strong>screen:</strong><code>system "screen -S mc -X quit"; system "screen -dmS mc java ..."</code></li>
|
||||
<li><strong>Windows:</strong>删除 root 检查,替换为 <code>spawn "cmd /c start /B java ..."</code>,日志输出到文件</li>
|
||||
<li><strong>监控其他服务:</strong>修改 <code>test_port</code> 的 IP 和端口即可</li>
|
||||
</ul>
|
||||
<p style="margin-top: 12px; color: var(--text-dim);">详细适配指南见 <a href="https://gitee.com/SCU_team/mine-sentinel/blob/master/DEPLOY.md" style="color: var(--orange);">DEPLOY.md</a></p>
|
||||
</div>
|
||||
|
||||
<!-- Gitee Widget -->
|
||||
<div class="widget-container">
|
||||
<div id="osc-gitee-widget-tag"></div>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="cta-section">
|
||||
<button onclick="openModal()" class="btn btn-primary">⬇️ 获取 MineSentinel</button>
|
||||
<a href="https://gitee.com/SCU_team/mine-sentinel" class="btn">⭐ Gitee 仓库</a>
|
||||
<a href="https://gitee.com/SCU_team/mine-sentinel/blob/master/DEPLOY.md" class="btn">📖 完整文档</a>
|
||||
</div>
|
||||
|
||||
<!-- Download Modal -->
|
||||
<div id="dlModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="modal-close" onclick="closeModal()">×</span>
|
||||
<h2 class="modal-title">⬇️ 获取 MineSentinel</h2>
|
||||
<div class="modal-body">
|
||||
<div class="download-info">
|
||||
<p><strong>Git 克隆:</strong></p>
|
||||
<pre style="margin:8px 0;font-size:0.85rem;">git clone https://gitee.com/SCU_team/mine-sentinel.git</pre>
|
||||
<p style="margin-top:10px;"><strong>直接下载:</strong></p>
|
||||
<p><a href="https://gitee.com/SCU_team/mine-sentinel/repository/archive/master.zip" target="_blank" class="download-link">📦 下载 ZIP 压缩包</a></p>
|
||||
<div class="password-info" style="margin-top: 12px;">
|
||||
<strong>📋 要求:</strong> Ruby >= 3.0 · Linux (systemd) 或 Windows
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: 16px; color: var(--text-dim); font-size: 0.9em;">💡 部署前请阅读 <a href="https://gitee.com/SCU_team/mine-sentinel/blob/master/DEPLOY.md" style="color: var(--orange);">DEPLOY.md</a>。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>© 2026 MineSentinel — S.A. SNOWARE-SCU</p>
|
||||
<p>源码采用 <a href="http://license.coscl.org.cn/MulanPubL-2.0" style="color:#fcd34d;">MulanPubL v2.0</a> · 品牌标识(Logo/SVG)保留所有权利</p>
|
||||
<div class="links">
|
||||
<a href="https://gitee.com/SCU_team/mine-sentinel">Gitee</a>
|
||||
<a href="https://gitee.com/SCU_team/mine-sentinel/issues">问题反馈</a>
|
||||
<a href="https://gitee.com/SCU_team/mine-sentinel/blob/master/DEPLOY.md">部署文档</a>
|
||||
</div>
|
||||
<script src="swsec.js"></script></footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openModal() {
|
||||
document.getElementById('dlModal').classList.add('show');
|
||||
}
|
||||
function closeModal() {
|
||||
document.getElementById('dlModal').classList.remove('show');
|
||||
}
|
||||
window.onclick = function(e) {
|
||||
if (e.target === document.getElementById('dlModal')) closeModal();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -9,23 +9,23 @@ Minecraft 服务器守护进程
|
||||
|
||||
[](http://license.coscl.org.cn/MulanPubL-2.0)
|
||||
[](https://www.ruby-lang.org)
|
||||
[](#跨场景适配指南)
|
||||
[](docs/DEPLOY.md#跨场景适配指南)
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 复制文件
|
||||
mkdir -p /opt/minecraft/sentinel
|
||||
cp main.rb method_executor.rb server_status_detector.rb /opt/minecraft/sentinel/
|
||||
mkdir -p /opt/minecraft/sentinel/src
|
||||
cp src/main.rb src/method_executor.rb src/server_status_detector.rb src/systemd_notifier.rb src/config.rb src/updates.rb /opt/minecraft/sentinel/src/
|
||||
|
||||
# 安装 systemd 服务
|
||||
cp minesentinel.service /etc/systemd/system/
|
||||
cp src/minesentinel.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable minesentinel
|
||||
systemctl start minesentinel
|
||||
|
||||
# 手动运行(调试用)
|
||||
cd /opt/minecraft/sentinel
|
||||
cd /opt/minecraft/sentinel/src
|
||||
ruby main.rb
|
||||
```
|
||||
|
||||
@@ -61,11 +61,14 @@ MineSentinel 通过 `Before=minecraft.service` 确保先于 MC 启动,避免 M
|
||||
| `main.rb` | 主循环、冷却期控制、初始宽限期 |
|
||||
| `method_executor.rb` | 日志系统、systemctl 重启、计数器 |
|
||||
| `server_status_detector.rb` | TCP 端口检测模块 |
|
||||
| `systemd_notifier.rb` | systemd watchdog 心跳发送 |
|
||||
| `config.rb` | 可调参数配置 |
|
||||
| `updates.rb` | 更新检查模块 |
|
||||
| `minesentinel.service` | systemd 单元文件(独立源码) |
|
||||
|
||||
## 完整文档
|
||||
|
||||
参见 [DEPLOY.md](DEPLOY.md) 或 [官网](OfficialWebsite/index.html)。
|
||||
参见 [DEPLOY.md](docs/DEPLOY.md) 或 [官网](static/index.html)。
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
+20
-15
@@ -20,9 +20,13 @@ MineSentinel 是一个 Minecraft 服务器守护进程,每 1.5 秒检测服务
|
||||
|
||||
```
|
||||
/opt/minecraft/sentinel/ # 推荐部署目录
|
||||
├── main.rb # 主程序入口 —— 检测循环 + 冷却期 + 初始宽限期
|
||||
├── method_executor.rb # 日志系统 + 重启逻辑 + 重试计数器
|
||||
└── server_status_detector.rb # TCP 端口检测模块
|
||||
└── src/ # 源码目录
|
||||
├── main.rb # 主程序入口 —— 检测循环 + 冷却期 + 初始宽限期
|
||||
├── method_executor.rb # 日志系统 + 重启逻辑 + 重试计数器
|
||||
├── server_status_detector.rb # TCP 端口检测模块
|
||||
├── systemd_notifier.rb # systemd watchdog 心跳发送
|
||||
├── config.rb # 可调参数配置
|
||||
└── updates.rb # 更新检查模块
|
||||
|
||||
minesentinel.service # systemd 单元文件(项目源码中)
|
||||
```
|
||||
@@ -34,8 +38,8 @@ minesentinel.service # systemd 单元文件(项目源码中)
|
||||
### 1. 创建目录并复制文件
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/minecraft/sentinel
|
||||
cp main.rb method_executor.rb server_status_detector.rb /opt/minecraft/sentinel/
|
||||
mkdir -p /opt/minecraft/sentinel/src
|
||||
cp src/main.rb src/method_executor.rb src/server_status_detector.rb src/systemd_notifier.rb src/config.rb src/updates.rb /opt/minecraft/sentinel/src/
|
||||
```
|
||||
|
||||
### 2. 确认 Minecraft systemd 服务存在
|
||||
@@ -49,7 +53,7 @@ systemctl status minecraft
|
||||
### 3. 手动测试
|
||||
|
||||
```bash
|
||||
cd /opt/minecraft/sentinel
|
||||
cd /opt/minecraft/sentinel/src
|
||||
ruby main.rb
|
||||
# 调试模式:LOG_LEVEL=debug ruby main.rb
|
||||
```
|
||||
@@ -59,7 +63,7 @@ ruby main.rb
|
||||
项目源码中已提供 `minesentinel.service`,直接复制:
|
||||
|
||||
```bash
|
||||
cp minesentinel.service /etc/systemd/system/
|
||||
cp src/minesentinel.service /etc/systemd/system/
|
||||
```
|
||||
|
||||
单元文件内容(`minesentinel.service`):
|
||||
@@ -73,8 +77,8 @@ Wants=minecraft.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/minecraft/sentinel
|
||||
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/main.rb
|
||||
WorkingDirectory=/opt/minecraft/sentinel/src
|
||||
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/src/main.rb
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
@@ -132,9 +136,10 @@ MineSentinel 启动:
|
||||
|
||||
| 参数 | 文件 | 行 | 默认值 |
|
||||
|------|------|-----|--------|
|
||||
| 初始宽限期 | `main.rb` | 31 | `300` s |
|
||||
| 检测间隔 | `main.rb` | 69 | `1.5` s |
|
||||
| 冷却期 | `main.rb` | 58 | `200` s |
|
||||
| 初始宽限期 | `config.rb` | 18 | `300` s |
|
||||
| 冷却期 | `config.rb` | 22 | `200` s |
|
||||
| 尝试次数 | `config.rb` | 20 | `3` 次 |
|
||||
| 检测间隔 | `config.rb` | 24 | `1.5` s |
|
||||
| systemctl 超时 | `method_executor.rb` | 17 | `30` s |
|
||||
| 最大重试 | `method_executor.rb` | 23 | `10` 次 |
|
||||
| 检测端口 | `server_status_detector.rb` | 60 | `25565` |
|
||||
@@ -199,8 +204,8 @@ pid = spawn "cmd /c start /B java -Xmx4G -jar C:\\mc\\server.jar nogui"
|
||||
|
||||
```ruby
|
||||
# server_status_detector.rb 第 60 行
|
||||
res = test_port('192.168.1.100', 3306) # MySQL
|
||||
res = test_port('127.0.0.1', 80) # Nginx
|
||||
res = ServerStatusDetector::TcpServer.test_port('192.168.1.100', 3306) # MySQL
|
||||
res = ServerStatusDetector::TcpServer.test_port('127.0.0.1', 80) # Nginx
|
||||
```
|
||||
|
||||
配合修改 `method_executor.rb` 中的重启命令即可适配。
|
||||
@@ -217,4 +222,4 @@ res = test_port('127.0.0.1', 80) # Nginx
|
||||
|
||||
## 许可证
|
||||
|
||||
MineSentinel 源码采用 [木兰宽松许可证 v2](http://license.coscl.org.cn/MulanPubL-2.0)。品牌标识(Logo / 项目名称)保留所有权利,未经许可不得用于衍生项目。
|
||||
MineSentinel 源码采用 [木兰公共许可证 v2](http://license.coscl.org.cn/MulanPubL-2.0)。品牌标识(Logo / 项目名称)保留所有权利,未经许可不得用于衍生项目。
|
||||
@@ -1,74 +0,0 @@
|
||||
# <<*>> -^v- SNOWARE APPLICATION
|
||||
# usage: MINESENTINEL mainloop.
|
||||
# author: S.A.
|
||||
# time: 2026-06-07
|
||||
# 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.
|
||||
|
||||
# main.rb 顶部添加
|
||||
begin
|
||||
# 原有代码
|
||||
rescue => e
|
||||
# 强制输出到stderr
|
||||
$stderr.puts "[CRITICAL] Unhandled exception: #{e.message}"
|
||||
$stderr.puts e.backtrace.join("\n")
|
||||
exit 1
|
||||
end
|
||||
|
||||
require_relative 'method_executor' # 日志系统在此时初始化
|
||||
require_relative 'server_status_detector'
|
||||
|
||||
MethodExecutor.record("info", "mainloop", "MineSentinel started, entering mainloop.")
|
||||
|
||||
# 初次启动:MineSentinel 先于 minecraft 启动(systemd Before=),
|
||||
# 等待服务器端口就绪,避免误判为崩溃
|
||||
INITIAL_GRACE = 300 # 初次启动等待 300 秒
|
||||
MethodExecutor.record("info", "mainloop",
|
||||
"Initial startup grace period #{INITIAL_GRACE}s — waiting for server to come up...")
|
||||
grace_deadline = Time.now + INITIAL_GRACE
|
||||
while Time.now < grace_deadline
|
||||
sleep 5
|
||||
if ServerStatusDetector::MinecraftServer.self_diag[:type]
|
||||
MethodExecutor.record("info", "mainloop",
|
||||
"Server port is open during grace period, resuming normal monitoring.")
|
||||
break
|
||||
end
|
||||
end
|
||||
MethodExecutor.record("info", "mainloop", "Grace period ended.")
|
||||
|
||||
# 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
|
||||
# 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟)
|
||||
cooldown = 200 # 200秒冷却期
|
||||
MethodExecutor.record("info", "mainloop", "Cooling down #{cooldown}s for server startup...")
|
||||
sleep cooldown
|
||||
MethodExecutor.record("info", "mainloop", "Cooldown finished, resuming monitoring.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
while true
|
||||
sleep 1.5
|
||||
main
|
||||
end
|
||||
@@ -1,124 +0,0 @@
|
||||
# <<*>> -^v- SNOWARE APPLICATION
|
||||
# usage: Automatic operator implementation.
|
||||
# author: S.A.
|
||||
# time: 2026-06-07
|
||||
# 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
|
||||
|
||||
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.
|
||||
STDOUT.sync = true
|
||||
STDERR.sync = true
|
||||
# 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,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 秒
|
||||
# @type const CHECK_INTERVAL: Float
|
||||
CHECK_INTERVAL = 1.5 # 检测间隔 1.5 秒
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# <<*>> -^v- SNOWARE APPLICATION
|
||||
# usage: MINESENTINEL mainloop.
|
||||
# author: S.A.
|
||||
# time: 2026-06-07
|
||||
# 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
|
||||
|
||||
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
||||
def main
|
||||
puts ' ___ __ ___ ___ ___ '
|
||||
puts ' |\/| | |\ | |__ /__` |__ |\ | | | |\ | |__ | '
|
||||
puts ' | | | | \| |___ .__/ |___ | \| | | | \| |___ |___ '
|
||||
puts ' MineSentinel dev-rolling version by <*> S.A. [@SNOWARE] and owned by SCU team.'
|
||||
puts ' Copyleft (c) 2026 S.A. SNOWARE-SCU. This program is licensed under Mulan PubL v2.'
|
||||
puts ' E-mail: SALflake@qq.com | QQ: 758522192 | Website: https://swe-iss.rth1.xyz/softwares/minesentinel.'
|
||||
puts
|
||||
|
||||
require_relative 'method_executor' # 日志系统在此时初始化
|
||||
MethodExecutor.record('debug', 'init',
|
||||
'Initialized MethodExecutor, which implements logging system and intervening functions.')
|
||||
require_relative 'server_status_detector'
|
||||
require_relative 'systemd_notifier' # 心跳系统在此时初始化
|
||||
require_relative 'config'
|
||||
MethodExecutor.record('debug', 'init', 'Initialized SystemdNotifier, which implements systemd watchdog feeder.')
|
||||
|
||||
# 确认是否具有展开干预操作的权限 如果因场景不同不需要请注释这部分内容
|
||||
unless Process.euid.zero?
|
||||
MethodExecutor.record('error', 'init', 'Permission denied: root required to intervene Systemd Services.')
|
||||
raise Errno::EPERM
|
||||
end
|
||||
|
||||
MethodExecutor.record('info', 'mainloop', 'MineSentinel started, entering mainloop.')
|
||||
|
||||
require_relative 'updates'
|
||||
|
||||
begin
|
||||
upd_result = UpdateManager.check
|
||||
if upd_result[:type] == :NewerAvailable
|
||||
MethodExecutor.record('info', 'updates',
|
||||
"Update #{upd_result[:version]} available, please check the git repo for details.")
|
||||
end
|
||||
rescue StandardError => e
|
||||
MethodExecutor.record('error', 'updates', e.to_s)
|
||||
end
|
||||
|
||||
# 初次启动:MineSentinel 先于 minecraft 启动(systemd Before=),
|
||||
# 等待服务器端口就绪,避免误判为崩溃
|
||||
MethodExecutor.record('info', 'mainloop',
|
||||
"Initial startup grace period #{INITIAL_GRACE}s — waiting for server to come up...")
|
||||
grace_deadline = Time.now + INITIAL_GRACE
|
||||
while Time.now < grace_deadline
|
||||
sleep 5
|
||||
SystemdNotifier.notify('WATCHDOG=1')
|
||||
next unless ServerStatusDetector::MinecraftServer.self_diag[:type]
|
||||
|
||||
MethodExecutor.record('info', 'mainloop',
|
||||
'Server port is open during grace period, resuming normal monitoring.')
|
||||
break
|
||||
end
|
||||
MethodExecutor.record('info', 'mainloop', 'Grace period ended.')
|
||||
main_loop
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
||||
# Functional mainloop.
|
||||
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
def main_once
|
||||
chkrslts = []
|
||||
(0...TRY_TIMES).each do
|
||||
chkrslt = ServerStatusDetector::MinecraftServer.self_diag
|
||||
chkrslts.append(chkrslt)
|
||||
sleep 0.5 unless chkrslt[:type] # 只在失败时等待后再重试
|
||||
end
|
||||
if chkrslts.all? { |chkrslt_in| chkrslt_in[:type] }
|
||||
MethodExecutor.record('debug', 'mainloop', 'Server health check passed.')
|
||||
MethodExecutor.reset_rst_counter
|
||||
else
|
||||
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] || []).include?('Record') }
|
||||
MethodExecutor.record('warn', 'mainloop', 'Action: Record -> Server died!')
|
||||
if chkrslts.any? { |chkrslt_in| (chkrslt_in[:operation] || []).include?('ForceResetServerProcess') }
|
||||
MethodExecutor.record('info', 'mainloop', 'Action: ForceResetServerProcess triggered.')
|
||||
if MethodExecutor.reset_minecraft_server
|
||||
# 重启后等待服务器启动(MC服务器启动通常需要30秒~2分钟)
|
||||
cooldown_beg = Time.now
|
||||
MethodExecutor.record('info', 'mainloop', "Cooling down #{RST_COOLDOWN_DURATION}s for server startup...")
|
||||
# rubocop:disable Metrics/BlockNesting
|
||||
while Time.now < cooldown_beg + RST_COOLDOWN_DURATION
|
||||
sleep 1
|
||||
SystemdNotifier.notify('WATCHDOG=1')
|
||||
end
|
||||
# rubocop:enable Metrics/BlockNesting
|
||||
MethodExecutor.record('info', 'mainloop', 'Cooldown finished, resuming monitoring.')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
def main_loop
|
||||
loop do
|
||||
sleep CHECK_INTERVAL
|
||||
SystemdNotifier.notify('WATCHDOG=1')
|
||||
main_once
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
main
|
||||
rescue StandardError => e
|
||||
# 强制输出到stderr
|
||||
warn "[CRITICAL] Unhandled exception: #{e.message}"
|
||||
warn e.backtrace&.join("\n")
|
||||
exit 1
|
||||
end
|
||||
@@ -0,0 +1,140 @@
|
||||
# <<*>> -^v- SNOWARE APPLICATION
|
||||
# usage: Automatic operator implementation.
|
||||
# author: S.A.
|
||||
# time: 2026-06-07
|
||||
# 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
|
||||
|
||||
PERSISTENCE_PERIOD = 30 # Wait for systemctl to take the order.
|
||||
|
||||
require 'logger'
|
||||
require_relative 'server_status_detector'
|
||||
|
||||
# rubocop:disable Style/ClassVars
|
||||
class MethodExecutor
|
||||
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.
|
||||
$stdout.sync = true
|
||||
$stderr.sync = true
|
||||
# Fetch the environmental variable and convert it into log level.
|
||||
@@logger.level = case ENV.fetch('LOG_LEVEL', nil) # @为单个实例(半生不熟的概念,貌似是在不同的空间独立创建),@@为共享同一个
|
||||
when nil, 'debug' # Default config.
|
||||
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'
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
|
||||
# @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'
|
||||
@@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
|
||||
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
|
||||
# @return [Boolean] true if a restart was attempted, false if recovered from idle
|
||||
def self.reset_minecraft_server
|
||||
record('info', 'reset', "Attempt ##{@@rst_counter + 1} to reset Minecraft server.")
|
||||
|
||||
if @@rst_counter >= RST_COUNTS_LIMIT
|
||||
record('error', 'reset',
|
||||
"Tried #{RST_COUNTS_LIMIT} times and failed, now I'll idle until server is manually recovered.")
|
||||
loop do
|
||||
sleep 10
|
||||
SystemdNotifier.notify('WATCHDOG=1') if defined?(SystemdNotifier)
|
||||
next unless ServerStatusDetector::MinecraftServer.self_diag[:type] == true
|
||||
|
||||
record('info', 'reset', 'Server recovered! Resuming normal monitoring.')
|
||||
@@rst_counter = 0
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
@@rst_counter += 1
|
||||
|
||||
unless Process.euid.zero?
|
||||
record('error', 'reset', 'Permission denied: root required to manage systemd services.')
|
||||
raise Errno::EPERM
|
||||
end
|
||||
|
||||
record('info', 'reset', "Spawning 'systemctl restart minecraft' (PID will be captured)...")
|
||||
pid = spawn 'systemctl restart minecraft'
|
||||
record('debug', 'reset', "systemctl restart spawned with PID #{pid}.")
|
||||
tick_beg = Time.now
|
||||
|
||||
# rubocop:disable Metrics/BlockLength
|
||||
loop do
|
||||
sleep 0.5
|
||||
SystemdNotifier.notify('WATCHDOG=1')
|
||||
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?
|
||||
record('info', 'reset', "'systemctl restart' succeeded in #{elapsed}s.")
|
||||
else
|
||||
record('error', 'reset',
|
||||
"'systemctl restart' failed with exitcode #{status.exitstatus} after #{elapsed}s.")
|
||||
end
|
||||
break
|
||||
else # 子进程仍在运行 (WNOHANG 返回 nil)
|
||||
if Time.now - tick_beg > PERSISTENCE_PERIOD
|
||||
record('warn', 'reset',
|
||||
"'systemctl restart' timed out after #{PERSISTENCE_PERIOD}s. Force-killing PID #{pid}...")
|
||||
system "kill -9 #{pid}"
|
||||
record('warn', 'reset', "Running 'systemctl kill minecraft' (last resort)...")
|
||||
system 'systemctl kill minecraft' # 没招了,毁灭吧
|
||||
sleep 0.5
|
||||
record('info', 'reset', "Running 'systemctl start minecraft' (rebirth)...")
|
||||
system 'systemctl start minecraft' # 重生
|
||||
record('info', 'reset', 'Force-reset sequence completed.')
|
||||
break
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Resets the restart counter when server health check passes.
|
||||
# @return [void]
|
||||
def self.reset_rst_counter
|
||||
@@rst_counter = 0
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
# rubocop:enable Style/ClassVars
|
||||
end
|
||||
@@ -13,8 +13,8 @@ Wants=minecraft.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/minecraft/sentinel
|
||||
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/main.rb
|
||||
WorkingDirectory=/opt/minecraft/sentinel/src
|
||||
ExecStart=/usr/bin/ruby /opt/minecraft/sentinel/src/main.rb
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
@@ -19,8 +19,12 @@ module ServerStatusDetector
|
||||
require 'timeout'
|
||||
|
||||
class TcpServer
|
||||
|
||||
def self.test_port(host = '127.0.0.1', port = 25565, timeout = 10)
|
||||
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
||||
# @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 = 25_565, timeout = 2)
|
||||
result = {
|
||||
port: port,
|
||||
status: :unknown,
|
||||
@@ -30,12 +34,11 @@ module ServerStatusDetector
|
||||
# 带错误处理的代码需要使用 begin...end
|
||||
begin
|
||||
Timeout.timeout(timeout) do # 此方法接收整数和代码块作为参数,并在超时时引发错误
|
||||
socket = TCPSocket.new(host,port)
|
||||
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
|
||||
@@ -44,57 +47,68 @@ module ServerStatusDetector
|
||||
result[:status] = :interrupted
|
||||
rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH
|
||||
result[:status] = :unreachable
|
||||
rescue => error
|
||||
rescue StandardError => e
|
||||
result[:status] = :error
|
||||
result[:error] = error.message
|
||||
result[:error] = e.message
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
||||
end
|
||||
|
||||
class MinecraftServer < TcpServer # 继承为TcpServer编写的属性和方法
|
||||
# 继承为TcpServer编写的属性和方法
|
||||
class MinecraftServer < TcpServer
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# @return [Hash{Symbol=>Boolean, Symbol=>Array<String>, Symbol=>String}]
|
||||
def self.self_diag
|
||||
res = test_port('127.0.0.1', 25_565)
|
||||
|
||||
def self.self_diag()
|
||||
|
||||
res = test_port('127.0.0.1', 25565)
|
||||
|
||||
if res[:error] != nil
|
||||
unless 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,
|
||||
operation: %w[Record],
|
||||
message: message
|
||||
}
|
||||
end # 立刻错误处理是好习惯
|
||||
# 立刻错误处理是好习惯
|
||||
end
|
||||
|
||||
case res[:status]
|
||||
|
||||
when :open
|
||||
MethodExecutor.record("debug", "self_diag",
|
||||
MethodExecutor.record('debug', 'self_diag',
|
||||
"TCP check OK — #{res[:response_time]}s")
|
||||
return {
|
||||
type: true,
|
||||
{
|
||||
type: true
|
||||
}
|
||||
when :timeout
|
||||
MethodExecutor.record("warn", "self_diag",
|
||||
"TCP check timed out.")
|
||||
return {
|
||||
MethodExecutor.record('warn', 'self_diag',
|
||||
'TCP check timed out.')
|
||||
{
|
||||
type: false,
|
||||
operation: ["Record"],
|
||||
message: "We sent packet without response."
|
||||
operation: %w[Record],
|
||||
message: 'We sent packet without response.'
|
||||
}
|
||||
when :closed
|
||||
MethodExecutor.record("warn", "self_diag",
|
||||
"TCP port closed — server is down.")
|
||||
return {
|
||||
MethodExecutor.record('warn', 'self_diag',
|
||||
'TCP port closed — server is down.')
|
||||
{
|
||||
type: false,
|
||||
operation: ["Record", "ForceResetServerProcess"], # 重启时注意要设置最大尝试次数
|
||||
message: "Server died for unknown reason, and will be restarted."
|
||||
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]}.")
|
||||
{
|
||||
type: false,
|
||||
operation: %w[Record],
|
||||
message: "Unexpected status: #{res[:status]}."
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
# <<*>> -^v- SNOWARE APPLICATION
|
||||
# usage: Notify systemd.
|
||||
# author: S.A.
|
||||
# time: 2026-06-07
|
||||
# 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
|
||||
|
||||
# Sends keep-alive notifications (including WATCHDOG=1) to systemd
|
||||
# via the NOTIFY_SOCKET datagram socket, enabling service supervision.
|
||||
class SystemdNotifier
|
||||
require 'socket'
|
||||
require_relative 'method_executor'
|
||||
|
||||
|
||||
@notify_socket = ENV.fetch('NOTIFY_SOCKET', nil)
|
||||
@watchdog_usec = (ENV['WATCHDOG_USEC'] || '0').to_i
|
||||
|
||||
if @notify_socket && File.socket?(@notify_socket)
|
||||
MethodExecutor.record('debug', 'heartbeat', 'Found NOTIFY_SOCKET, enabling heartbeat')
|
||||
@enable_heartbeat = true
|
||||
# Fix(2026-06-08): 将 UNIXSocket(SOCK_STREAM) 改为 SOCK_DGRAM
|
||||
# systemd NOTIFY_SOCKET 是 datagram socket,用 stream socket 连接会报
|
||||
# Protocol wrong type for socket (Errno::EPROTOTYPE)
|
||||
@socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
|
||||
@socket.connect(Socket.pack_sockaddr_un(@notify_socket))
|
||||
MethodExecutor.record('debug', 'heartbeat', 'Heartbeat socket opened')
|
||||
else
|
||||
MethodExecutor.record('error', 'heartbeat', 'NOTIFY_SOCKET is not set or is not a socket, am I a service?')
|
||||
@enable_heartbeat = false
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
def self.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 StandardError => e
|
||||
MethodExecutor.record('error', 'heartbeat', "Failed to send heartbeat: #{e}")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# <<*>> -^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 = 'd494093'
|
||||
# @type const CHANNEL: String
|
||||
CHANNEL = 'dev'
|
||||
# @type const VERSION_LIST_URL: String
|
||||
VERSION_LIST_URL = "https://gitee.com/SCU_team/mine-sentinel/raw/#{CHANNEL}/APP_VERSIONS_LIST"
|
||||
# @type const RECURSIVE_RETRY_DEPTH: Integer
|
||||
RECURSIVE_RETRY_DEPTH = 3
|
||||
|
||||
class UpdateManager
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# @return [String, nil]
|
||||
def self.fetch
|
||||
MethodExecutor.record('debug', 'updates', "Fetching version list from #{VERSION_LIST_URL}")
|
||||
uri = URI(VERSION_LIST_URL)
|
||||
5.times do # 跟随重定向(Gitee raw URL → raw.giteeusercontent.com)
|
||||
response = Net::HTTP.get_response(uri)
|
||||
case response.code.to_i
|
||||
when 200
|
||||
MethodExecutor.record('debug', 'updates', 'Version list fetched successfully.')
|
||||
return response.body
|
||||
when 301, 302, 307, 308
|
||||
uri = URI(response['location'])
|
||||
MethodExecutor.record('debug', 'updates', "Following redirect to #{uri}")
|
||||
next
|
||||
else
|
||||
raise "Unexpected HTTP response code: #{response.code}"
|
||||
end
|
||||
end
|
||||
raise 'Too many redirects'
|
||||
rescue StandardError => e
|
||||
MethodExecutor.record('error', 'updates', "Failed to fetch version list: #{e}")
|
||||
nil
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# @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}), use git to update.")
|
||||
{ type: :NewerAvailable, version: latest }
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
@@ -3,7 +3,7 @@ name = "MineSentinel"
|
||||
description = "Minecraft 服务器智能守护进程,TCP 端口检测 + 崩溃自动重启 + 防循环冷却期"
|
||||
repo_url = "https://gitee.com/snoware/minesentinel"
|
||||
demo_url = "https://swe-iss.rth1.xyz/softwares/minesentinel"
|
||||
doc_url = "https://gitee.com/snoware/minesentinel/blob/master/DEPLOY.md"
|
||||
doc_url = "https://gitee.com/snoware/minesentinel/blob/dev/docs/DEPLOY.md"
|
||||
tags = ["Ruby", "Minecraft", "Server", "Monitoring", "Daemon", "SNOWARE"]
|
||||
|
||||
[[projects]]
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user